Implements likes fully in frontend and backend.
This commit is contained in:
parent
5c9f1bb85e
commit
a0655e049d
5 changed files with 169 additions and 19 deletions
93
forum.nim
93
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
@ -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"):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue