Implements likes fully in frontend and backend.

This commit is contained in:
Dominik Picheta 2018-05-18 17:04:34 +01:00
commit a0655e049d
5 changed files with 169 additions and 19 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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
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"))

View file

@ -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"):