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],
|
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(
|
return Post(
|
||||||
id: postRow[0].parseInt,
|
id: postRow[0].parseInt,
|
||||||
replyingTo: replyingTo,
|
replyingTo: replyingTo,
|
||||||
author: selectUser(@[postRow[5], postRow[6], postRow[7], postRow[8]]),
|
author: selectUser(@[postRow[5], postRow[6], postRow[7], postRow[8]]),
|
||||||
likes: @[], # TODO:
|
likes: likes,
|
||||||
seen: false, # TODO:
|
seen: false, # TODO:
|
||||||
history: history,
|
history: history,
|
||||||
info: PostInfo(
|
info: PostInfo(
|
||||||
|
|
@ -940,6 +941,18 @@ proc selectHistory(postId: int): seq[PostInfo] =
|
||||||
content: row[1].rstToHtml()
|
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 =
|
proc selectThread(threadRow: seq[string]): Thread =
|
||||||
const postsQuery =
|
const postsQuery =
|
||||||
sql"""select count(*), strftime('%s', creation) from post
|
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"])
|
raise newForumError("Couldn't send activation email", @["email"])
|
||||||
|
|
||||||
# Add account to person table
|
# Add account to person table
|
||||||
exec(db,
|
exec(db, sql"""
|
||||||
sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " &
|
INSERT INTO person(name, password, email, salt, status, lastOnline)
|
||||||
"VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name,
|
VALUES (?, ?, ?, ?, ?, DATETIME('now'))
|
||||||
password, email, salt, $EmailUnconfirmed)
|
""", name, password, email, salt, $EmailUnconfirmed)
|
||||||
|
|
||||||
return password
|
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()
|
initialise()
|
||||||
|
|
||||||
routes:
|
routes:
|
||||||
|
|
@ -1256,8 +1300,11 @@ routes:
|
||||||
|
|
||||||
if addDetail:
|
if addDetail:
|
||||||
let replyingTo = selectReplyingTo(rows[i][4])
|
let replyingTo = selectReplyingTo(rows[i][4])
|
||||||
let history = selectHistory(rows[i][0].parseInt())
|
let history = selectHistory(id)
|
||||||
let post = selectPost(rows[i], skippedPosts, replyingTo, history)
|
let likes = selectLikes(id)
|
||||||
|
let post = selectPost(
|
||||||
|
rows[i], skippedPosts, replyingTo, history, likes
|
||||||
|
)
|
||||||
list.posts.add(post)
|
list.posts.add(post)
|
||||||
skippedPosts = @[]
|
skippedPosts = @[]
|
||||||
else:
|
else:
|
||||||
|
|
@ -1285,7 +1332,8 @@ routes:
|
||||||
|
|
||||||
for row in db.getAllRows(postsQuery):
|
for row in db.getAllRows(postsQuery):
|
||||||
let history = selectHistory(row[0].parseInt())
|
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"
|
resp $(%list), "application/json"
|
||||||
|
|
||||||
|
|
@ -1570,6 +1618,33 @@ routes:
|
||||||
except ForumError as exc:
|
except ForumError as exc:
|
||||||
resp Http400, $(%exc.data), "application/json"
|
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":
|
get "/t/@id":
|
||||||
cond "id" in request.params
|
cond "id" in request.params
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -366,6 +366,10 @@ $views-color: #545d70;
|
||||||
.like-button i:hover, .like-button i.fas {
|
.like-button i:hover, .like-button i.fas {
|
||||||
color: #f783ac;
|
color: #f783ac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.like-count {
|
||||||
|
margin-right: $control-padding-x-sm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#thread-buttons {
|
#thread-buttons {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,16 @@ proc isModerated*(post: Post): bool =
|
||||||
## Determines whether the specified thread is under moderation.
|
## Determines whether the specified thread is under moderation.
|
||||||
post.author.rank <= Moderated
|
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):
|
when defined(js):
|
||||||
import karaxutils
|
import karaxutils
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ when defined(js):
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import karax/[kajax, kdom]
|
import karax/[kajax, kdom]
|
||||||
|
|
||||||
import error, karaxutils
|
import error, karaxutils, post, user
|
||||||
|
|
||||||
type
|
type
|
||||||
PostButton* = ref object
|
PostButton* = ref object
|
||||||
|
|
@ -74,4 +74,68 @@ when defined(js):
|
||||||
|
|
||||||
if state.error.isSome():
|
if state.error.isSome():
|
||||||
p(class="text-error"):
|
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
|
include karax/prelude
|
||||||
import karax / [vstyles, kajax, kdom]
|
import karax / [vstyles, kajax, kdom]
|
||||||
|
|
||||||
import karaxutils, error, replybox, editbox
|
import karaxutils, error, replybox, editbox, postbutton
|
||||||
|
|
||||||
type
|
type
|
||||||
State = ref object
|
State = ref object
|
||||||
|
|
@ -28,6 +28,7 @@ when defined(js):
|
||||||
replyBox: ReplyBox
|
replyBox: ReplyBox
|
||||||
editing: Option[Post] ## If in edit mode, this contains the post.
|
editing: Option[Post] ## If in edit mode, this contains the post.
|
||||||
editBox: EditBox
|
editBox: EditBox
|
||||||
|
likeButton: LikeButton
|
||||||
|
|
||||||
proc onReplyPosted(id: int)
|
proc onReplyPosted(id: int)
|
||||||
proc onEditPosted(id: int, content: string, subject: Option[string])
|
proc onEditPosted(id: int, content: string, subject: Option[string])
|
||||||
|
|
@ -39,7 +40,8 @@ when defined(js):
|
||||||
status: Http200,
|
status: Http200,
|
||||||
replyingTo: none[Post](),
|
replyingTo: none[Post](),
|
||||||
replyBox: newReplyBox(onReplyPosted),
|
replyBox: newReplyBox(onReplyPosted),
|
||||||
editBox: newEditBox(onEditPosted, onEditCancelled)
|
editBox: newEditBox(onEditPosted, onEditCancelled),
|
||||||
|
likeButton: newLikeButton()
|
||||||
)
|
)
|
||||||
|
|
||||||
var
|
var
|
||||||
|
|
@ -175,12 +177,7 @@ when defined(js):
|
||||||
button(class="btn"):
|
button(class="btn"):
|
||||||
italic(class="far fa-trash-alt")
|
italic(class="far fa-trash-alt")
|
||||||
|
|
||||||
tdiv(class="like-button"):
|
render(state.likeButton, post, currentUser)
|
||||||
button(class="btn"):
|
|
||||||
span(class="like-count"):
|
|
||||||
if post.likes.len > 0:
|
|
||||||
text $post.likes.len
|
|
||||||
italic(class="far fa-heart")
|
|
||||||
|
|
||||||
if loggedIn:
|
if loggedIn:
|
||||||
tdiv(class="flag-button"):
|
tdiv(class="flag-button"):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue