From a0655e049de50262187e6d2e95f4da75218b2732 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 17:04:34 +0100 Subject: [PATCH] Implements likes fully in frontend and backend. --- forum.nim | 93 +++++++++++++++++++++++++++++++++++++---- frontend/nimforum.scss | 4 ++ frontend/post.nim | 10 +++++ frontend/postbutton.nim | 68 +++++++++++++++++++++++++++++- frontend/postlist.nim | 13 +++--- 5 files changed, 169 insertions(+), 19 deletions(-) diff --git a/forum.nim b/forum.nim index 3633ac1..924d657 100644 --- a/forum.nim +++ b/forum.nim @@ -889,12 +889,13 @@ proc selectUser(userRow: seq[string], avatarSize: int=80): User = ) proc selectPost(postRow: seq[string], skippedPosts: seq[int], - replyingTo: Option[PostLink], history: seq[PostInfo]): Post = + replyingTo: Option[PostLink], history: seq[PostInfo], + likes: seq[User]): Post = return Post( id: postRow[0].parseInt, replyingTo: replyingTo, author: selectUser(@[postRow[5], postRow[6], postRow[7], postRow[8]]), - likes: @[], # TODO: + likes: likes, seen: false, # TODO: history: history, info: PostInfo( @@ -940,6 +941,18 @@ proc selectHistory(postId: int): seq[PostInfo] = content: row[1].rstToHtml() )) +proc selectLikes(postId: int): seq[User] = + const likeQuery = sql""" + select u.name, u.email, strftime('%s', u.lastOnline), u.status + from like h, person u + where h.post = ? and h.author = u.id + order by h.creation asc; + """ + + result = @[] + for row in getAllRows(db, likeQuery, $postId): + result.add(selectUser(row)) + proc selectThread(threadRow: seq[string]): Thread = const postsQuery = sql"""select count(*), strftime('%s', creation) from post @@ -1169,13 +1182,44 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, raise newForumError("Couldn't send activation email", @["email"]) # Add account to person table - exec(db, - sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " & - "VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name, - password, email, salt, $EmailUnconfirmed) + exec(db, sql""" + INSERT INTO person(name, password, email, salt, status, lastOnline) + VALUES (?, ?, ?, ?, ?, DATETIME('now')) + """, name, password, email, salt, $EmailUnconfirmed) return password +proc executeLike(c: TForumData, postId: int) = + # Verify the post exists and doesn't belong to the current user. + const postQuery = sql""" + select u.name from post p, person u + where p.id = ? and p.author = u.id and p.isDeleted = 0; + """ + + let postAuthor = getValue(db, postQuery, postId) + if postAuthor.len == 0: + raise newForumError("Specified post ID does not exist.", @["id"]) + + if postAuthor == c.username: + raise newForumError("You cannot like your own post.") + + # Save the like. + exec(db, crud(crCreate, "like", "author", "post"), c.userid, postId) + +proc executeUnlike(c: TForumData, postId: int) = + # Verify the post and like exists for the current user. + const likeQuery = sql""" + select l.id from like l, person u + where l.post = ? and l.author = u.id and u.name = ?; + """ + + let likeId = getValue(db, likeQuery, postId, c.username) + if likeId.len == 0: + raise newForumError("Like doesn't exist.", @["id"]) + + # Delete the like. + exec(db, crud(crDelete, "like"), likeId) + initialise() routes: @@ -1256,8 +1300,11 @@ routes: if addDetail: let replyingTo = selectReplyingTo(rows[i][4]) - let history = selectHistory(rows[i][0].parseInt()) - let post = selectPost(rows[i], skippedPosts, replyingTo, history) + let history = selectHistory(id) + let likes = selectLikes(id) + let post = selectPost( + rows[i], skippedPosts, replyingTo, history, likes + ) list.posts.add(post) skippedPosts = @[] else: @@ -1285,7 +1332,8 @@ routes: for row in db.getAllRows(postsQuery): let history = selectHistory(row[0].parseInt()) - list.add(selectPost(row, @[], selectReplyingTo(row[4]), history)) + let likes = selectLikes(row[0].parseInt()) + list.add(selectPost(row, @[], selectReplyingTo(row[4]), history, likes)) resp $(%list), "application/json" @@ -1570,6 +1618,33 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post re"/(like|unlike)": + 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 postId = getInt(formData["id"].body, -1) + cond postId != -1 + + try: + case request.path + of "/like": + executeLike(c, postId) + of "/unlike": + executeUnlike(c, postId) + else: + assert false + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + get "/t/@id": cond "id" in request.params diff --git a/frontend/nimforum.scss b/frontend/nimforum.scss index 2fc3ad3..143eaad 100644 --- a/frontend/nimforum.scss +++ b/frontend/nimforum.scss @@ -366,6 +366,10 @@ $views-color: #545d70; .like-button i:hover, .like-button i.fas { color: #f783ac; } + + .like-count { + margin-right: $control-padding-x-sm; + } } #thread-buttons { diff --git a/frontend/post.nim b/frontend/post.nim index 77b97f8..7e46a6b 100644 --- a/frontend/post.nim +++ b/frontend/post.nim @@ -35,6 +35,16 @@ proc isModerated*(post: Post): bool = ## Determines whether the specified thread is under moderation. post.author.rank <= Moderated +proc isLikedBy*(post: Post, user: Option[User]): bool = + ## Determines whether the specified user has liked the post. + if user.isNone(): return false + + for u in post.likes: + if u.name == user.get().name: + return true + + return false + when defined(js): import karaxutils diff --git a/frontend/postbutton.nim b/frontend/postbutton.nim index 913b392..8bc89ef 100644 --- a/frontend/postbutton.nim +++ b/frontend/postbutton.nim @@ -8,7 +8,7 @@ when defined(js): include karax/prelude import karax/[kajax, kdom] - import error, karaxutils + import error, karaxutils, post, user type PostButton* = ref object @@ -74,4 +74,68 @@ when defined(js): if state.error.isSome(): p(class="text-error"): - text state.error.get().message \ No newline at end of file + text state.error.get().message + + + type + LikeButton* = ref object + error: Option[PostError] + loading: bool + + proc newLikeButton*(): LikeButton = + LikeButton() + + proc onPost(httpStatus: int, response: kstring, state: LikeButton, + post: Post, user: User) = + postFinished: + if post.isLikedBy(some(user)): + var newLikes: seq[User] = @[] + for like in post.likes: + if like.name != user.name: + newLikes.add(like) + post.likes = newLikes + else: + post.likes.add(user) + + proc onClick(ev: Event, n: VNode, state: LikeButton, post: Post, + currentUser: Option[User]) = + if state.loading: return + if currentUser.isNone(): + state.error = some[PostError](PostError(message: "Not logged in.")) + + state.loading = true + state.error = none[PostError]() + + # TODO: This is a hack, karax should support this. + var formData = newFormData() + formData.append("id", $post.id) + let uri = + if post.isLikedBy(currentUser): + makeUri("/unlike") + else: + makeUri("/like") + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => + onPost(s, r, state, post, currentUser.get())) + + ev.preventDefault() + + proc render*(state: LikeButton, post: Post, + currentUser: Option[User]): VNode = + + let liked = isLikedBy(post, currentUser) + let tooltip = + if state.error.isSome(): state.error.get().message + else: "" + + result = buildHtml(): + tdiv(class="like-button"): + button(class=class({"tooltip": state.error.isSome()}, "btn"), + onClick=(e: Event, n: VNode) => + (onClick(e, n, state, post, currentUser)), + "data-tooltip"=tooltip): + if post.likes.len > 0: + span(class="like-count"): + text $post.likes.len + + italic(class=class({"far": not liked, "fas": liked}, "fa-heart")) \ No newline at end of file diff --git a/frontend/postlist.nim b/frontend/postlist.nim index b323d1e..13c91d3 100644 --- a/frontend/postlist.nim +++ b/frontend/postlist.nim @@ -17,7 +17,7 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, error, replybox, editbox + import karaxutils, error, replybox, editbox, postbutton type State = ref object @@ -28,6 +28,7 @@ when defined(js): replyBox: ReplyBox editing: Option[Post] ## If in edit mode, this contains the post. editBox: EditBox + likeButton: LikeButton proc onReplyPosted(id: int) proc onEditPosted(id: int, content: string, subject: Option[string]) @@ -39,7 +40,8 @@ when defined(js): status: Http200, replyingTo: none[Post](), replyBox: newReplyBox(onReplyPosted), - editBox: newEditBox(onEditPosted, onEditCancelled) + editBox: newEditBox(onEditPosted, onEditCancelled), + likeButton: newLikeButton() ) var @@ -175,12 +177,7 @@ when defined(js): button(class="btn"): italic(class="far fa-trash-alt") - tdiv(class="like-button"): - button(class="btn"): - span(class="like-count"): - if post.likes.len > 0: - text $post.likes.len - italic(class="far fa-heart") + render(state.likeButton, post, currentUser) if loggedIn: tdiv(class="flag-button"):