comment edit spagetti
This commit is contained in:
parent
fec2a6ecc0
commit
b4be2c613b
@ -159,11 +159,35 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!parentId) {
|
if (!parentId) {
|
||||||
throw new UserInputError('comment must have parent', { argumentName: 'text' })
|
throw new UserInputError('comment must have parent', { argumentName: 'parentId' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return await createItem(parent, { text, parentId }, { me, models })
|
return await createItem(parent, { text, parentId }, { me, models })
|
||||||
},
|
},
|
||||||
|
updateComment: async (parent, { id, text }, { me, models }) => {
|
||||||
|
if (!text) {
|
||||||
|
throw new UserInputError('comment must have text', { argumentName: 'text' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw new UserInputError('comment must have id', { argumentName: 'id' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// update iff this comment belongs to me
|
||||||
|
const comment = await models.item.findUnique({ where: { id: Number(id) } })
|
||||||
|
if (Number(comment.userId) !== Number(me.id)) {
|
||||||
|
throw new AuthenticationError('comment must belong to you')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() > new Date(comment.createdAt).getTime() + 10 * 60000) {
|
||||||
|
throw new UserInputError('comment can no longer be editted')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await models.item.update({
|
||||||
|
where: { id: Number(id) },
|
||||||
|
data: { text }
|
||||||
|
})
|
||||||
|
},
|
||||||
vote: async (parent, { id, sats = 1 }, { me, models }) => {
|
vote: async (parent, { id, sats = 1 }, { me, models }) => {
|
||||||
// need to make sure we are logged in
|
// need to make sure we are logged in
|
||||||
if (!me) {
|
if (!me) {
|
||||||
|
@ -13,6 +13,7 @@ export default gql`
|
|||||||
createLink(title: String!, url: String): Item!
|
createLink(title: String!, url: String): Item!
|
||||||
createDiscussion(title: String!, text: String): Item!
|
createDiscussion(title: String!, text: String): Item!
|
||||||
createComment(text: String!, parentId: ID!): Item!
|
createComment(text: String!, parentId: ID!): Item!
|
||||||
|
updateComment(id: ID!, text: String!): Item!
|
||||||
vote(id: ID!, sats: Int): Int!
|
vote(id: ID!, sats: Int): Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
79
components/comment-edit.js
Normal file
79
components/comment-edit.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Form, MarkdownInput, SubmitButton } from '../components/form'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { gql, useMutation } from '@apollo/client'
|
||||||
|
import styles from './reply.module.css'
|
||||||
|
import TextareaAutosize from 'react-textarea-autosize'
|
||||||
|
import Countdown from 'react-countdown'
|
||||||
|
|
||||||
|
export const CommentSchema = Yup.object({
|
||||||
|
text: Yup.string().required('required').trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
|
||||||
|
const [updateComment] = useMutation(
|
||||||
|
gql`
|
||||||
|
mutation updateComment($id: ID! $text: String!) {
|
||||||
|
updateComment(id: $id, text: $text) {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}`, {
|
||||||
|
update (cache, { data: { updateComment } }) {
|
||||||
|
cache.modify({
|
||||||
|
id: `Item:${comment.id}`,
|
||||||
|
fields: {
|
||||||
|
text () {
|
||||||
|
return updateComment.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.reply} mt-2`}>
|
||||||
|
<Form
|
||||||
|
initial={{
|
||||||
|
text: comment.text
|
||||||
|
}}
|
||||||
|
schema={CommentSchema}
|
||||||
|
onSubmit={async (values, { resetForm }) => {
|
||||||
|
const { error } = await updateComment({ variables: { ...values, id: comment.id } })
|
||||||
|
if (error) {
|
||||||
|
throw new Error({ message: error.toString() })
|
||||||
|
}
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MarkdownInput
|
||||||
|
name='text'
|
||||||
|
as={TextareaAutosize}
|
||||||
|
minRows={4}
|
||||||
|
autoFocus
|
||||||
|
required
|
||||||
|
groupClassName='mb-0'
|
||||||
|
hint={
|
||||||
|
<span className='text-muted font-weight-bold'>
|
||||||
|
<Countdown
|
||||||
|
date={editThreshold}
|
||||||
|
renderer={props => <span> {props.formatted.minutes}:{props.formatted.seconds}</span>}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className='d-flex align-items-center justify-content-between'>
|
||||||
|
<SubmitButton variant='secondary' className='mt-1'>save</SubmitButton>
|
||||||
|
<div
|
||||||
|
className='font-weight-bold text-muted mr-3'
|
||||||
|
style={{ fontSize: '80%', cursor: 'pointer' }}
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -9,6 +9,9 @@ import UpVote from './upvote'
|
|||||||
import Eye from '../svgs/eye-fill.svg'
|
import Eye from '../svgs/eye-fill.svg'
|
||||||
import EyeClose from '../svgs/eye-close-line.svg'
|
import EyeClose from '../svgs/eye-close-line.svg'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
import { useMe } from './me'
|
||||||
|
import CommentEdit from './comment-edit'
|
||||||
|
import Countdown from 'react-countdown'
|
||||||
|
|
||||||
function Parent ({ item }) {
|
function Parent ({ item }) {
|
||||||
const ParentFrag = () => (
|
const ParentFrag = () => (
|
||||||
@ -37,9 +40,15 @@ function Parent ({ item }) {
|
|||||||
|
|
||||||
export default function Comment ({ item, children, replyOpen, includeParent, cacheId, noComments, noReply, clickToContext }) {
|
export default function Comment ({ item, children, replyOpen, includeParent, cacheId, noComments, noReply, clickToContext }) {
|
||||||
const [reply, setReply] = useState(replyOpen)
|
const [reply, setReply] = useState(replyOpen)
|
||||||
|
const [edit, setEdit] = useState()
|
||||||
const [collapse, setCollapse] = useState(false)
|
const [collapse, setCollapse] = useState(false)
|
||||||
const ref = useRef(null)
|
const ref = useRef(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const me = useMe()
|
||||||
|
const mine = me.id === item.user.id
|
||||||
|
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||||
|
const [canEdit, setCanEdit] =
|
||||||
|
useState(mine && (Date.now() < editThreshold))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Number(router.query.commentId) === Number(item.id)) {
|
if (Number(router.query.commentId) === Number(item.id)) {
|
||||||
@ -81,23 +90,63 @@ export default function Comment ({ item, children, replyOpen, includeParent, cac
|
|||||||
: <EyeClose className={styles.collapser} height={10} width={10} onClick={() => setCollapse(true)} />)}
|
: <EyeClose className={styles.collapser} height={10} width={10} onClick={() => setCollapse(true)} />)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.text}>
|
{edit
|
||||||
<Text>{item.text}</Text>
|
? (
|
||||||
</div>
|
<div className={styles.replyWrapper}>
|
||||||
|
<CommentEdit
|
||||||
|
comment={item}
|
||||||
|
onSuccess={() => {
|
||||||
|
setEdit(!edit)
|
||||||
|
setCanEdit(mine && (Date.now() < editThreshold))
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setEdit(!edit)
|
||||||
|
setCanEdit(mine && (Date.now() < editThreshold))
|
||||||
|
}}
|
||||||
|
editThreshold={editThreshold}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className={styles.text}>
|
||||||
|
<Text>{item.text}</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${itemStyles.children} ${styles.children}`}>
|
<div className={`${itemStyles.children} ${styles.children}`}>
|
||||||
{!noReply &&
|
{!noReply && !edit && (
|
||||||
<div
|
<div className={`${itemStyles.other} ${styles.reply}`}>
|
||||||
className={`${itemStyles.other} ${styles.reply}`}
|
<div
|
||||||
onClick={() => setReply(!reply)}
|
className='d-inline-block'
|
||||||
>
|
onClick={() => setReply(!reply)}
|
||||||
{reply ? 'cancel' : 'reply'}
|
>
|
||||||
</div>}
|
{reply ? 'cancel' : 'reply'}
|
||||||
|
</div>
|
||||||
|
{canEdit && !reply && !edit &&
|
||||||
|
<>
|
||||||
|
<span> \ </span>
|
||||||
|
<div
|
||||||
|
className='d-inline-block'
|
||||||
|
onClick={() => setEdit(!edit)}
|
||||||
|
>
|
||||||
|
edit
|
||||||
|
<Countdown
|
||||||
|
date={editThreshold}
|
||||||
|
renderer={props => <span> {props.formatted.minutes}:{props.formatted.seconds}</span>}
|
||||||
|
onComplete={() => {
|
||||||
|
setCanEdit(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={reply ? styles.replyWrapper : 'd-none'}>
|
<div className={reply ? styles.replyWrapper : 'd-none'}>
|
||||||
<Reply
|
<Reply
|
||||||
parentId={item.id} autoFocus={!replyOpen}
|
parentId={item.id} autoFocus={!replyOpen}
|
||||||
onSuccess={() => setReply(replyOpen || false)} cacheId={cacheId}
|
onSuccess={() => setReply(replyOpen || false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
|
@ -45,7 +45,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.children {
|
.children {
|
||||||
margin-top: .25rem;
|
margin-top: 0;
|
||||||
|
padding-top: .25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comments {
|
.comments {
|
||||||
|
@ -89,8 +89,8 @@ export function MarkdownInput ({ label, groupClassName, ...props }) {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='form-group'>
|
<div className={tab !== 'preview' ? 'd-none' : 'form-group'}>
|
||||||
<div className={tab !== 'preview' ? 'd-none' : `${styles.text} form-control`}>
|
<div className={`${styles.text} form-control`}>
|
||||||
<Text>{meta.value}</Text>
|
<Text>{meta.value}</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,7 +53,6 @@
|
|||||||
.skeleton .other {
|
.skeleton .other {
|
||||||
height: 17px;
|
height: 17px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton .title {
|
.skeleton .title {
|
||||||
|
@ -47,7 +47,7 @@ export default function Reply ({ parentId, onSuccess, autoFocus }) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.reply}>
|
<div className={`${styles.reply} mb-1`}>
|
||||||
<Form
|
<Form
|
||||||
initial={{
|
initial={{
|
||||||
text: ''
|
text: ''
|
||||||
|
@ -8,6 +8,7 @@ export const COMMENT_FIELDS = gql`
|
|||||||
text
|
text
|
||||||
user {
|
user {
|
||||||
name
|
name
|
||||||
|
id
|
||||||
}
|
}
|
||||||
sats
|
sats
|
||||||
boost
|
boost
|
||||||
|
@ -9,6 +9,7 @@ export const ITEM_FIELDS = gql`
|
|||||||
url
|
url
|
||||||
user {
|
user {
|
||||||
name
|
name
|
||||||
|
id
|
||||||
}
|
}
|
||||||
sats
|
sats
|
||||||
boost
|
boost
|
||||||
|
21
package-lock.json
generated
21
package-lock.json
generated
@ -28,6 +28,7 @@
|
|||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
"react-bootstrap": "^1.5.2",
|
"react-bootstrap": "^1.5.2",
|
||||||
|
"react-countdown": "^2.3.2",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"react-markdown": "^6.0.2",
|
"react-markdown": "^6.0.2",
|
||||||
"react-syntax-highlighter": "^15.4.3",
|
"react-syntax-highlighter": "^15.4.3",
|
||||||
@ -8552,6 +8553,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
|
||||||
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
|
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-countdown": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 15",
|
||||||
|
"react-dom": ">= 15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "17.0.1",
|
"version": "17.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",
|
||||||
@ -17996,6 +18009,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-countdown": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"version": "17.0.1",
|
"version": "17.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
"react-bootstrap": "^1.5.2",
|
"react-bootstrap": "^1.5.2",
|
||||||
|
"react-countdown": "^2.3.2",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"react-markdown": "^6.0.2",
|
"react-markdown": "^6.0.2",
|
||||||
"react-syntax-highlighter": "^15.4.3",
|
"react-syntax-highlighter": "^15.4.3",
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
-- Only update path if we have conditions that require us to reset it
|
||||||
|
CREATE OR REPLACE FUNCTION update_item_path() RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
npath ltree;
|
||||||
|
BEGIN
|
||||||
|
IF NEW."parentId" IS NULL THEN
|
||||||
|
SELECT NEW.id::text::ltree INTO npath;
|
||||||
|
NEW."path" = npath;
|
||||||
|
ELSEIF TG_OP = 'INSERT' OR OLD."parentId" IS NULL OR OLD."parentId" != NEW."parentId" THEN
|
||||||
|
SELECT "path" || NEW.id::text FROM "Item" WHERE id = NEW."parentId" INTO npath;
|
||||||
|
IF npath IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Invalid parent_id %', NEW."parentId";
|
||||||
|
END IF;
|
||||||
|
NEW."path" = npath;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
Loading…
x
Reference in New Issue
Block a user