Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rjwebb/add sentiment check comments #39

Merged
merged 11 commits into from
Apr 23, 2024
Merged
5 changes: 5 additions & 0 deletions client/src/pages/dashboard/conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import api from "../../util/api"
import Survey, { surveyBox } from "../survey"
import { populateZidMetadataStore } from "../../actions"
import { SentimentCheck } from "./sentiment_check"
import { SentimentCheckComments } from "./sentiment_check_comments"
import { Frontmatter, Collapsible } from "./front_matter"
import { MIN_SEED_RESPONSES } from "./index"

Expand Down Expand Up @@ -205,6 +206,10 @@ export const DashboardConversation = ({
zid_metadata={zid_metadata}
key={zid_metadata.conversation_id}
/>
<SentimentCheckComments
user={user}
conversationId={zid_metadata.conversation_id}
/>
</Box>
)}
{!zid_metadata.github_pr_title &&
Expand Down
111 changes: 111 additions & 0 deletions client/src/pages/dashboard/sentiment_check_comments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useCallback, useState } from "react"
import { Box, Flex, Image, Link, Text } from "theme-ui";
import useSWR from "swr";

const fetcher = url => fetch(url).then(r => r.json())

export const SentimentCheckComments: React.FC<{ user; conversationId: string }> = ({
user,
conversationId,
}) => {
const { data, mutate } = useSWR(
`/api/v3/conversation/sentiment_comments?conversation_id=${conversationId}`,
fetcher
)
const sentimentComments = data || []
const [comment, setComment] = useState("")

const submitComment = useCallback((comment: string) => {
fetch(`/api/v3/conversation/sentiment_comments?conversation_id=${conversationId}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
comment,
}),
}).then(() => {
mutate()
setComment("")
})
}, [conversationId])

const deleteComment = useCallback((commentId: number) => {
fetch(`/api/v3/conversation/sentiment_comments?comment_id=${commentId}`, {
method: "DELETE",
}).then(() => {
mutate()
})
}, [])

return (
<Flex sx={{flexDirection:"column", gap: "8px"}}>
<Text
sx={{
fontWeight: "bold",
}}
>
Comments
</Text>
{
sentimentComments.length > 0 ?
<Flex
sx={{
flexDirection: "column",
gap: "8px",
}}
>
{sentimentComments.map((comment) => (
<Flex
sx={{
flexDirection: "column",
padding: "8px",
gap: "6px"
}}
key={comment.id}
>
{comment.is_deleted ? <Text>[deleted]</Text> :
<>
<Flex
sx={{
flexDirection: "row",
alignItems: "center"
}}
>
<Link href={`https://github.com/${comment.github_username}`} target="_blank">
<Image
src={`https://github.com/${comment.github_username}.png`}
width="34"
height="34"
style={{
borderRadius: 6,
marginRight: "8px",
background: "#ccc",
marginTop: "2px",
}}
/>
</Link>
<Text sx={{flexGrow: 1}}>{comment.github_username} - {new Date(parseInt(comment.created)).toLocaleString()}</Text>
{comment.can_delete && <Text sx={{cursor: "pointer"}} onClick={() => deleteComment(comment.id)}>Delete</Text>}
</Flex>
<Box>{comment.comment}</Box>
</>}
</Flex>
))}
</Flex>
:
<>
No comments yet
</>
}

<input
type="text"
placeholder="Comment"
onChange={(e) => setComment(e.target.value)}
value={comment}
/>
<button onClick={() => submitComment(comment)}>Submit</button>
</Flex>
)
}
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ import {
wantCookie,
wantHeader,
} from "./src/utils/parameter";
import {
handle_DELETE_conversation_sentiment_check_comments,
handle_GET_conversation_sentiment_comments,
handle_POST_conversation_sentiment_check_comments
} from "./src/handlers/sentiment_check_comments";

const app = express();

Expand Down Expand Up @@ -841,6 +846,31 @@ app.put(
handle_PUT_conversations,
);

app.get(
"/api/v3/conversation/sentiment_comments",
moveToBody,
authOptional(assignToP),
need("conversation_id", getConversationIdFetchZid, assignToPCustom("zid")),
handle_GET_conversation_sentiment_comments as any
)

app.post(
"/api/v3/conversation/sentiment_comments",
moveToBody,
auth(assignToP),
need("conversation_id", getConversationIdFetchZid, assignToPCustom("zid")),
need("comment", getStringLimitLength(999), assignToP),
handle_POST_conversation_sentiment_check_comments as any
)

app.delete(
"/api/v3/conversation/sentiment_comments",
moveToBody,
auth(assignToP),
need("comment_id", getInt, assignToP),
handle_DELETE_conversation_sentiment_check_comments as any
)

app.put(
"/api/v3/users",
moveToBody,
Expand Down
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"serve-favicon": "^2.5.0",
"simple-oauth2": "~0.2.1",
"sql": "~0.34.0",
"swr": "^2.2.5",
"underscore": "~1.12.1",
"uuid": "^3.1.0",
"valid-url": "~1.0.9",
Expand Down
10 changes: 10 additions & 0 deletions server/postgres/migrations/000033_add_sentiment_check_comments.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE sentiment_check_comments(
id SERIAL,
zid INTEGER NOT NULL REFERENCES conversations(zid),
uid INTEGER NOT NULL REFERENCES users(uid),
created BIGINT DEFAULT now_as_millis(),
comment TEXT,
is_deleted BOOLEAN DEFAULT FALSE
);

CREATE INDEX sentiment_check_comments_zid_idx ON sentiment_check_comments USING btree (zid);
103 changes: 103 additions & 0 deletions server/src/handlers/sentiment_check_comments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Request, Response } from "express";
import { queryP_readOnly } from "../db/pg-query";
import fail from "../utils/fail";
import { isAdministrator } from "../user";


export async function handle_GET_conversation_sentiment_comments (req: Request & {p: any}, res: Response) {
// make sure that this query does not return the zid
const query = `
SELECT
scc.comment,
scc.created,
scc.id,
scc.uid,
scc.is_deleted,
u.github_username
FROM
sentiment_check_comments as scc
JOIN
users u
ON
u.uid = scc.uid
WHERE
scc.zid = $1
ORDER BY
scc.created ASC;
`;

let queryResult;
try {
queryResult = await queryP_readOnly(query.toString(), [req.p.zid]);
} catch (err) {
fail(res, 500, "polis_err_get_conversation_sentiment_comments", err);
return;
}

const userIsAdministrator = isAdministrator(req.p.uid);

const result = queryResult.map((comment) => {
// redact the content of comments that have been deleted, unless the user is an admin
if(comment.is_deleted) {
return {
id: comment.id,
comment: "[deleted]",
github_username: "[deleted]",
created: comment.created,
can_delete: false,
is_deleted: true
};
} else {
return {...comment, can_delete: comment.uid === req.p.uid || userIsAdministrator}
}
});

res.status(200).json(result);
}

export async function handle_POST_conversation_sentiment_check_comments (req: Request & {p: any}, res: Response) {
const query = "INSERT INTO sentiment_check_comments (zid, uid, comment) VALUES ($1, $2, $3) RETURNING *;";

let result;
try {
result = await queryP_readOnly(query.toString(), [req.p.zid, req.p.uid, req.p.comment]);
} catch (err) {
fail(res, 500, "polis_err_post_conversation_sentiment_check_comments", err);
return;
}

res.status(201).json(result);
}

export async function handle_DELETE_conversation_sentiment_check_comments (req: Request & {p: any}, res: Response) {
const selectQuery = "SELECT zid, uid, comment FROM sentiment_check_comments WHERE id = $1;";

let comments;
try {
comments = await queryP_readOnly(selectQuery.toString(), [req.p.comment_id]);
} catch (err) {
fail(res, 500, "polis_err_delete_conversation_sentiment_check_comments", err);
return;
}

if(comments.length == 0) {
fail(res, 404, "polis_err_delete_conversation_sentiment_check_comments", "sentiment check comment not found");
return;
}
const comment = comments[0]

if(comment.uid !== req.p.uid) {
fail(res, 403, "polis_err_delete_conversation_sentiment_check_comments", "user not authorized to delete this comment");
return;
}

const deleteQuery = "UPDATE sentiment_check_comments SET is_deleted = true WHERE id = $1;";
try {
await queryP_readOnly(deleteQuery.toString(), [req.p.comment_id]);
} catch (err) {
fail(res, 500, "polis_err_delete_conversation_sentiment_check_comments", err);
return;
}

res.status(200).json(`comment with id ${req.p.comment_id} deleted`);
}
Loading