diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 144f557..3bcc459 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -241,6 +241,10 @@ $views-color: #545d70; margin: 0; } + + i.fas { + margin-right: $control-padding-x-sm; + } } .posts, .about { @@ -384,6 +388,7 @@ $views-color: #545d70; .btn { float: right; margin-right: 0; + margin-left: $control-padding-x; } } diff --git a/src/forum.nim b/src/forum.nim index afc4eee..b7b191f 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -419,10 +419,28 @@ proc executeReply(c: TForumData, threadId: int, content: string, if rateLimitCheck(c): raise newForumError("You're posting too fast!") + if content.strip().len == 0: + raise newForumError("Message cannot be empty") + if not validateRst(c, content): raise newForumError("Message needs to be valid RST", @["msg"]) - # Verify that content can be parsed as RST. + # Ensure that the thread isn't locked. + let isLocked = getValue( + db, + sql""" + select isLocked from thread where id in ( + select thread from post where id = ? + ) + """, + threadId + ) + if isLocked.len == 0: + raise newForumError("Thread not found.") + + if isLocked == "1": + raise newForumError("Cannot reply to a locked thread.") + let retID = insertID( db, crud(crCreate, "post", "author", "ip", "content", "thread", "replyingTo"), @@ -630,6 +648,14 @@ proc executeUnlike(c: TForumData, postId: int) = # Delete the like. exec(db, crud(crDelete, "like"), likeId) +proc executeLockState(c: TForumData, threadId: int, locked: bool) = + # Verify that the logged in user has the correct permissions. + if c.rank < Moderator: + raise newForumError("You cannot lock this thread.") + + # Save the like. + exec(db, crud(crUpdate, "thread", "isLocked"), locked.int, threadId) + proc executeDeletePost(c: TForumData, postId: int) = # Verify that this post belongs to the user. const postQuery = sql""" @@ -1127,6 +1153,33 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post re"/(lock|unlock)": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "id" in formData + + let threadId = getInt(formData["id"].body, -1) + cond threadId != -1 + + try: + case request.path + of "/lock": + executeLockState(c, threadId, true) + of "/unlock": + executeLockState(c, threadId, false) + else: + assert false + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + post re"/delete(Post|Thread)": createTFD() if not c.loggedIn(): diff --git a/src/frontend/postbutton.nim b/src/frontend/postbutton.nim index 5c35357..673bf62 100644 --- a/src/frontend/postbutton.nim +++ b/src/frontend/postbutton.nim @@ -8,7 +8,7 @@ when defined(js): include karax/prelude import karax/[kajax, kdom] - import error, karaxutils, post, user + import error, karaxutils, post, user, threadlist type PostButton* = ref object @@ -142,4 +142,61 @@ when defined(js): span(class="like-count"): text $post.likes.len - italic(class=class({"far": not liked, "fas": liked}, "fa-heart")) \ No newline at end of file + italic(class=class({"far": not liked, "fas": liked}, "fa-heart")) + + type + LockButton* = ref object + error: Option[PostError] + loading: bool + + proc newLockButton*(): LockButton = + LockButton() + + proc onPost(httpStatus: int, response: kstring, state: LockButton, + thread: var Thread) = + postFinished: + thread.isLocked = not thread.isLocked + + proc onLockClick(ev: Event, n: VNode, state: LockButton, thread: var Thread) = + if state.loading: return + + state.loading = true + state.error = none[PostError]() + + # TODO: This is a hack, karax should support this. + var formData = newFormData() + formData.append("id", $thread.id) + let uri = + if thread.isLocked: + makeUri("/unlock") + else: + makeUri("/lock") + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => + onPost(s, r, state, thread)) + + ev.preventDefault() + + proc render*(state: LockButton, thread: var Thread, + currentUser: Option[User]): VNode = + if currentUser.isNone() or + currentUser.get().rank < Moderator: + return buildHtml(tdiv()) + + let tooltip = + if state.error.isSome(): state.error.get().message + else: "" + + result = buildHtml(): + button(class="btn btn-secondary", + onClick=(e: Event, n: VNode) => + onLockClick(e, n, state, thread), + "data-tooltip"=tooltip, + onmouseleave=(e: Event, n: VNode) => + (state.error = none[PostError]())): + if thread.isLocked: + italic(class="fas fa-unlock-alt") + text " Unlock Thread" + else: + italic(class="fas fa-lock") + text " Lock Thread" \ No newline at end of file diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 84f3b7b..548d912 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -32,6 +32,7 @@ when defined(js): editBox: EditBox likeButton: LikeButton deleteModal: DeleteModal + lockButton: LockButton proc onReplyPosted(id: int) proc onEditPosted(id: int, content: string, subject: Option[string]) @@ -47,7 +48,8 @@ when defined(js): replyBox: newReplyBox(onReplyPosted), editBox: newEditBox(onEditPosted, onEditCancelled), likeButton: newLikeButton(), - deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil) + deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil), + lockButton: newLockButton() ) var @@ -364,6 +366,8 @@ when defined(js): italic(class="fas fa-reply") text " Reply" + render(state.lockButton, list.thread, currentUser) + render(state.replyBox, list.thread, state.replyingTo, false) render(state.deleteModal) \ No newline at end of file diff --git a/src/frontend/replybox.nim b/src/frontend/replybox.nim index 5a1e032..20e8114 100644 --- a/src/frontend/replybox.nim +++ b/src/frontend/replybox.nim @@ -121,7 +121,7 @@ when defined(js): value=state.text) if state.error.isSome(): - p(class="text-error", + span(class="text-error", style=style(StyleAttr.marginTop, "0.4rem")): text state.error.get().message