Change post visibility (#83)

* Change post visibility

Closes #64

* Fix imports + right align controls
This commit is contained in:
Joaquin "Florius" Azcarate 2022-04-14 23:27:38 +02:00 committed by GitHub
parent 5df56fbdae
commit b9d26e16f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 192 additions and 6 deletions

View file

@ -0,0 +1,111 @@
import PasswordModal from "@components/new-post/password-modal"
import { Button, ButtonGroup, Loading, useToasts } from "@geist-ui/core"
import type { PostVisibility } from "@lib/types"
import Cookies from "js-cookie"
import { useCallback, useState } from "react"
type Props = {
postId: string
visibility: PostVisibility
setVisibility: (visibility: PostVisibility) => void
}
const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => {
const [isSubmitting, setSubmitting] = useState(false)
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
const { setToast } = useToasts()
const sendRequest = useCallback(
async (visibility: PostVisibility, password?: string) => {
const res = await fetch(`/server-api/posts/${postId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("drift-token")}`
},
body: JSON.stringify({ visibility, password })
})
if (res.ok) {
const json = await res.json()
setVisibility(json.visibility)
} else {
const json = await res.json()
setToast({
text: json.error.message,
type: "error"
})
setPasswordModalVisible(false)
}
},
[setToast]
)
const onSubmit = useCallback(
async (visibility: PostVisibility, password?: string) => {
if (visibility === "protected" && !password) {
setPasswordModalVisible(true)
return
}
setPasswordModalVisible(false)
const timeout = setTimeout(() => setSubmitting(true), 100)
await sendRequest(visibility, password)
clearTimeout(timeout)
setSubmitting(false)
},
[sendRequest]
)
const onClosePasswordModal = () => {
setPasswordModalVisible(false)
setSubmitting(false)
}
const submitPassword = useCallback(
(password) => onSubmit("protected", password),
[onSubmit]
)
return (
<>
{isSubmitting ? (
<Loading />
) : (
<ButtonGroup margin={0}>
<Button
disabled={visibility === "private"}
onClick={() => onSubmit("private")}
>
Make private
</Button>
<Button
disabled={visibility === "public"}
onClick={() => onSubmit("public")}
>
Make Public
</Button>
<Button
disabled={visibility === "unlisted"}
onClick={() => onSubmit("unlisted")}
>
Unlist
</Button>
<Button onClick={() => onSubmit("protected")}>
{visibility === "protected"
? "Change Password"
: "Protect with password"}
</Button>
</ButtonGroup>
)}
<PasswordModal
creating={true}
isOpen={passwordModalVisible}
onClose={onClosePasswordModal}
onSubmit={submitPassword}
/>
</>
)
}
export default VisibilityControl

View file

@ -4,7 +4,7 @@ import DocumentComponent from "@components/view-document"
import styles from "./post-page.module.css" import styles from "./post-page.module.css"
import homeStyles from "@styles/Home.module.css" import homeStyles from "@styles/Home.module.css"
import type { File, Post } from "@lib/types" import type { File, Post, PostVisibility } from "@lib/types"
import { Page, Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core" import { Page, Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import Archive from "@geist-ui/icons/archive" import Archive from "@geist-ui/icons/archive"
@ -18,6 +18,7 @@ import CreatedAgoBadge from "@components/badges/created-ago-badge"
import Cookies from "js-cookie" import Cookies from "js-cookie"
import getPostPath from "@lib/get-post-path" import getPostPath from "@lib/get-post-path"
import PasswordModalPage from "./password-modal-wrapper" import PasswordModalPage from "./password-modal-wrapper"
import VisibilityControl from "@components/badges/visibility-control"
type Props = { type Props = {
post: Post post: Post
@ -26,18 +27,19 @@ type Props = {
const PostPage = ({ post: initialPost, isProtected }: Props) => { const PostPage = ({ post: initialPost, isProtected }: Props) => {
const [post, setPost] = useState<Post>(initialPost) const [post, setPost] = useState<Post>(initialPost)
const [visibility, setVisibility] = useState<PostVisibility>(post.visibility)
const [isExpired, setIsExpired] = useState( const [isExpired, setIsExpired] = useState(
post.expiresAt ? new Date(post.expiresAt) < new Date() : null post.expiresAt ? new Date(post.expiresAt) < new Date() : null
) )
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [isOwner] = useState(
post.users ? post.users[0].id === Cookies.get("drift-userid") : false
)
const router = useRouter() const router = useRouter()
const isMobile = useMediaQuery("mobile") const isMobile = useMediaQuery("mobile")
useEffect(() => { useEffect(() => {
const isOwner = post.users
? post.users[0].id === Cookies.get("drift-userid")
: false
if (!isOwner && isExpired) { if (!isOwner && isExpired) {
router.push("/expired") router.push("/expired")
} }
@ -59,7 +61,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
return () => { return () => {
if (interval) clearInterval(interval) if (interval) clearInterval(interval)
} }
}, [isExpired, post.expiresAt, post.users, router]) }, [isExpired, isOwner, post.expiresAt, post.users, router])
const download = async () => { const download = async () => {
if (!post.files) return if (!post.files) return
@ -143,7 +145,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
<span className={styles.title}> <span className={styles.title}>
<Text h3>{post.title}</Text> <Text h3>{post.title}</Text>
<span className={styles.badges}> <span className={styles.badges}>
<VisibilityBadge visibility={post.visibility} /> <VisibilityBadge visibility={visibility} />
<CreatedAgoBadge createdAt={post.createdAt} /> <CreatedAgoBadge createdAt={post.createdAt} />
<ExpirationBadge postExpirationDate={post.expiresAt} /> <ExpirationBadge postExpirationDate={post.expiresAt} />
</span> </span>
@ -164,6 +166,9 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
content={content} content={content}
/> />
))} ))}
{isOwner && <span className={styles.controls}>
<VisibilityControl postId={post.id} visibility={visibility} setVisibility={setVisibility} />
</span>}
<ScrollToTop /> <ScrollToTop />
</Page.Content> </Page.Content>
</Page> </Page>

View file

@ -22,6 +22,11 @@
margin-bottom: var(--gap); margin-bottom: var(--gap);
} }
.controls {
display: flex;
justify-content: flex-end;
}
@media screen and (max-width: 900px) { @media screen and (max-width: 900px) {
.header { .header {
flex-direction: column; flex-direction: column;

View file

@ -402,3 +402,68 @@ posts.delete("/:id", jwt, async (req: UserJwtRequest, res, next) => {
next(e) next(e)
} }
}) })
posts.put(
"/:id",
jwt,
celebrate({
params: {
id: Joi.string().required()
},
body: {
visibility: Joi.string()
.custom(postVisibilitySchema, "valid visibility")
.required(),
password: Joi.string().optional(),
}
}),
async (req: UserJwtRequest, res, next) => {
try {
const isUserAuthor = (post: Post) => {
return (
req.user?.id &&
post.users?.map((user) => user.id).includes(req.user?.id)
)
}
const { visibility, password } = req.body;
let hashedPassword: string = ""
if (visibility === "protected") {
hashedPassword = crypto
.createHash("sha256")
.update(password)
.digest("hex")
}
const { id } = req.params;
const post = await Post.findByPk(id, {
include: [
{
model: User,
as: "users",
attributes: ["id"]
},
]
})
if (!post) {
return res.status(404).json({ error: "Post not found" })
}
if (!isUserAuthor(post)) {
return res.status(403).json({ error: "This post does not belong to you" })
}
await Post.update(
{ password: hashedPassword, visibility },
{ where: { id } }
)
res.json({ id, visibility })
} catch (e) {
res.status(400).json(e)
}
}
)