diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 65b4fcff..005abedb 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -159,11 +159,35 @@ export default { } 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 }) }, + 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 }) => { // need to make sure we are logged in if (!me) { diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index c5d924cf..08980f17 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -13,6 +13,7 @@ export default gql` createLink(title: String!, url: String): Item! createDiscussion(title: String!, text: String): Item! createComment(text: String!, parentId: ID!): Item! + updateComment(id: ID!, text: String!): Item! vote(id: ID!, sats: Int): Int! } diff --git a/components/comment-edit.js b/components/comment-edit.js new file mode 100644 index 00000000..3bdc2b52 --- /dev/null +++ b/components/comment-edit.js @@ -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 ( +
+
{ + const { error } = await updateComment({ variables: { ...values, id: comment.id } }) + if (error) { + throw new Error({ message: error.toString() }) + } + if (onSuccess) { + onSuccess() + } + }} + > + + {props.formatted.minutes}:{props.formatted.seconds}} + /> + + } + /> +
+ save +
+ cancel +
+
+ +
+ ) +} diff --git a/components/comment.js b/components/comment.js index 56b2fd91..21f1069d 100644 --- a/components/comment.js +++ b/components/comment.js @@ -9,6 +9,9 @@ import UpVote from './upvote' import Eye from '../svgs/eye-fill.svg' import EyeClose from '../svgs/eye-close-line.svg' import { useRouter } from 'next/router' +import { useMe } from './me' +import CommentEdit from './comment-edit' +import Countdown from 'react-countdown' function Parent ({ item }) { const ParentFrag = () => ( @@ -37,9 +40,15 @@ function Parent ({ item }) { export default function Comment ({ item, children, replyOpen, includeParent, cacheId, noComments, noReply, clickToContext }) { const [reply, setReply] = useState(replyOpen) + const [edit, setEdit] = useState() const [collapse, setCollapse] = useState(false) const ref = useRef(null) 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(() => { if (Number(router.query.commentId) === Number(item.id)) { @@ -81,23 +90,63 @@ export default function Comment ({ item, children, replyOpen, includeParent, cac : setCollapse(true)} />)} -
- {item.text} -
+ {edit + ? ( +
+ { + setEdit(!edit) + setCanEdit(mine && (Date.now() < editThreshold)) + }} + onCancel={() => { + setEdit(!edit) + setCanEdit(mine && (Date.now() < editThreshold)) + }} + editThreshold={editThreshold} + /> +
+ ) + : ( +
+ {item.text} +
+ )}
- {!noReply && -
setReply(!reply)} - > - {reply ? 'cancel' : 'reply'} -
} + {!noReply && !edit && ( +
+
setReply(!reply)} + > + {reply ? 'cancel' : 'reply'} +
+ {canEdit && !reply && !edit && + <> + \ +
setEdit(!edit)} + > + edit + {props.formatted.minutes}:{props.formatted.seconds}} + onComplete={() => { + setCanEdit(false) + }} + /> +
+ } +
+ )} +
setReply(replyOpen || false)} cacheId={cacheId} + onSuccess={() => setReply(replyOpen || false)} />
{children} diff --git a/components/comment.module.css b/components/comment.module.css index aa9ed4f1..e04fd681 100644 --- a/components/comment.module.css +++ b/components/comment.module.css @@ -45,7 +45,8 @@ } .children { - margin-top: .25rem; + margin-top: 0; + padding-top: .25rem; } .comments { diff --git a/components/form.js b/components/form.js index 40210798..7879d125 100644 --- a/components/form.js +++ b/components/form.js @@ -89,8 +89,8 @@ export function MarkdownInput ({ label, groupClassName, ...props }) { {...props} />
-
-
+
+
{meta.value}
diff --git a/components/item.module.css b/components/item.module.css index b3283cff..112b857f 100644 --- a/components/item.module.css +++ b/components/item.module.css @@ -53,7 +53,6 @@ .skeleton .other { height: 17px; align-items: center; - display: flex; } .skeleton .title { diff --git a/components/reply.js b/components/reply.js index 93886d82..8ba87042 100644 --- a/components/reply.js +++ b/components/reply.js @@ -47,7 +47,7 @@ export default function Reply ({ parentId, onSuccess, autoFocus }) { ) return ( -
+
= 15", + "react-dom": ">= 15" + } + }, "node_modules/react-dom": { "version": "17.0.1", "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": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", diff --git a/package.json b/package.json index 90f2a4a1..4c233de1 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "qrcode.react": "^1.0.1", "react": "17.0.1", "react-bootstrap": "^1.5.2", + "react-countdown": "^2.3.2", "react-dom": "17.0.1", "react-markdown": "^6.0.2", "react-syntax-highlighter": "^15.4.3", diff --git a/prisma/migrations/20210810195449_fix_item_path_trigger/migration.sql b/prisma/migrations/20210810195449_fix_item_path_trigger/migration.sql new file mode 100644 index 00000000..ae2b313c --- /dev/null +++ b/prisma/migrations/20210810195449_fix_item_path_trigger/migration.sql @@ -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;