Implements post list in front end and backend.

This commit is contained in:
Dominik Picheta 2018-05-10 16:05:42 +01:00
commit 366bdadc90
7 changed files with 283 additions and 59 deletions

131
forum.nim
View file

@ -12,6 +12,7 @@ import
parseutils, utils, random, rst, ranks, recaptcha, json
import redesign/threadlist except User
import redesign/[category, postlist]
when not defined(windows):
import bcrypt # TODO
@ -1013,6 +1014,47 @@ template createTFD() =
if request.cookies.len > 0:
checkLoggedIn(c)
#[ DB functions. TODO: Move to another module? ]#
proc selectUser(userRow: seq[string]): threadlist.User =
let isOnline = getTime().toUnix() - userRow[2].parseInt > (60*5)
return threadlist.User(
name: userRow[0],
avatarUrl: userRow[1].getGravatarUrl(),
isOnline: isOnline
)
proc selectThread(threadRow: seq[string]): Thread =
const postsQuery =
sql"""select count(*), strftime('%s', creation) from post
where thread = ?
order by creation asc limit 1;"""
const usersListQuery =
sql"""select distinct name, email, strftime('%s', lastOnline)
from person where id in
(select author from post where thread = ?)
limit 5;""" # TODO: Order by most posts.
let posts = getRow(db, postsQuery, threadRow[0])
var thread = Thread(
id: threadRow[0].parseInt,
topic: threadRow[1],
category: Category(id: "", color: "#ff0000"), # TODO
users: @[],
replies: posts[0].parseInt,
views: threadRow[2].parseInt,
activity: threadRow[3].parseInt,
creation: posts[1].parseInt,
isLocked: false,
isSolved: false # TODO: ^ and this. Add a field to `post` to identify.
)
# Gather the users list.
for user in getAllRows(db, usersListQuery, thread.id):
thread.users.add(selectUser(user))
return thread
initialise()
@ -1037,56 +1079,71 @@ routes:
get "/karax/threads.json":
var
start = 0
count = 30
parseInt(@"start", start, 0..1_000_000)
parseInt(@"count", start, 0..1_000_000)
start = getInt(@"start", 0)
count = getInt(@"count", 30)
const threadsQuery =
sql"""select id, name, views, strftime('%s', modified) from thread
order by modified desc limit ?, ?;"""
const postsQuery =
sql"""select count(*), strftime('%s', creation) from post
where thread = ?
order by creation asc limit 1;"""
const usersListQuery =
sql"""select distinct name, email, strftime('%s', lastOnline)
from person where id in
(select author from post where thread = ?)
limit 5;""" # TODO: Order by most posts.
order by modified desc limit ?, ?;""" # TODO: Moderation
let thrCount = getValue(db, sql"select count(*) from thread;").parseInt()
let moreCount = max(0, thrCount - (start + count))
var list = ThreadList(threads: @[], lastVisit: 0, moreCount: moreCount)
for data in getAllRows(db, threadsQuery, start, count):
let posts = getRow(db, postsQuery, data[0])
var thread = Thread(
id: data[0].parseInt,
topic: data[1],
category: Category(id: "", color: "#ff0000"), # TODO
users: @[],
replies: posts[0].parseInt,
views: data[2].parseInt,
activity: data[3].parseInt,
creation: posts[1].parseInt,
isLocked: false,
isSolved: false # TODO: ^ and this. Add a field to `post` to identify.
)
# Gather the users list.
for user in getAllRows(db, usersListQuery, thread.id):
let isOnline = getTime().toUnix() - user[2].parseInt > (60*5)
thread.users.add(threadlist.User(
name: user[0],
avatarUrl: user[1].getGravatarUrl(),
isOnline: isOnline
))
let thread = selectThread(data)
list.threads.add(thread)
resp $(%list), "application/json"
get "/karax/posts.json":
createTFD()
var
id = getInt(@"id", -1)
start = getInt(@"start", 0)
count = getInt(@"count", 5)
cond id != -1
const threadsQuery =
sql"""select id, name, views, strftime('%s', modified) from thread
where id = ?;"""
let threadRow = getRow(db, threadsQuery, id)
let thread = selectThread(threadRow)
let modClause =
if c.rank >= Moderator:
"(1 or u.id = ?)"
else:
"(u.status <> 'Moderated' or p.author = ?)"
let postsQuery =
sql(
"""select p.id, p.content, strftime('%s', p.creation), p.author,
u.name, u.email, strftime('%s', u.lastOnline)
from post p, person u
where u.id = p.author and p.thread = ? and $#
and (u.status <> 'Spammer' or p.author = ?)
order by p.id limit ?, ?""" % modClause
)
var list = PostList(posts: @[], history: @[], thread: thread)
for post in getAllRows(db, postsQuery, id, c.userId, c.userId,
start, count):
list.posts.add(Post(
id: post[0].parseInt,
author: selectUser(@[post[4], post[5], post[6]]),
likes: @[], # TODO:
seen: false, # TODO:
history: @[], # TODO:
info: PostInfo(
creation: post[2].parseInt,
content: post[1]
)
))
resp $(%list), "application/json"
get "/threadActivity.xml":
createTFD()
c.isThreadsList = true

23
redesign/category.nim Normal file
View file

@ -0,0 +1,23 @@
type
Category* = object
id*: string
color*: string
when defined(js):
include karax/prelude
import karax / [vstyles, kajax, kdom]
import karaxutils
proc render*(category: Category): VNode =
result = buildHtml():
if category.id.len > 0:
tdiv(class="triangle",
style=style(
(StyleAttr.borderBottom, kstring"0.6rem solid " & category.color)
)):
text category.id
else:
span()

17
redesign/error.nim Normal file
View file

@ -0,0 +1,17 @@
include karax/prelude
import karax / [vstyles, kajax, kdom]
proc renderError*(message: string): VNode =
result = buildHtml():
tdiv(class="empty error"):
tdiv(class="empty icon"):
italic(class="fas fa-bug fa-5x")
p(class="empty-title h5"):
text message
p(class="empty-subtitle"):
text "Please report this issue to us so we can fix it!"
tdiv(class="empty-action"):
a(href="https://github.com/nim-lang/nimforum/issues", target="_blank"):
button(class="btn btn-primary"):
text "Report issue"

View file

@ -4,7 +4,7 @@ from dom import window, Location
include karax/prelude
import threadlist, karaxutils
import threadlist, postlist, karaxutils
type
State = ref object
@ -44,7 +44,7 @@ proc render(): VNode =
result = buildHtml(tdiv()):
genHeader()
if "/t/" in state.url.pathname:
text "</thread>"
renderPostList(3806, false)
else:
renderThreadList()

117
redesign/postlist.nim Normal file
View file

@ -0,0 +1,117 @@
import options, json, times, httpcore, strformat
import threadlist, category
type
PostInfo* = object
creation*: int64
content*: string
Post* = object
id*: int
author*: User
likes*: seq[User] ## Users that liked this post.
seen*: bool ## Determines whether the current user saw this post.
## I considered using a simple timestamp for each thread,
## but that wouldn't work when a user navigates to the last
## post in a thread for example.
history*: seq[PostInfo] ## If the post was edited this will contain the
## older versions of the post.
info*: PostInfo
PostList* = ref object
thread*: Thread
history*: seq[Thread] ## If the thread was edited this will contain the
## older versions of the thread (title/category
## changes).
posts*: seq[Post]
when defined(js):
include karax/prelude
import karax / [vstyles, kajax, kdom]
import karaxutils, error
type
State = ref object
list: Option[PostList]
loading: bool
status: HttpCode
proc newState(): State =
State(
list: none[PostList](),
loading: false,
status: Http200
)
var
state = newState()
proc onPostList(httpStatus: int, response: kstring) =
state.loading = false
state.status = httpStatus.HttpCode
if state.status != Http200: return
let parsed = parseJson($response)
let list = to(parsed, PostList)
if state.list.isSome:
state.list.get().posts.add(list.posts)
# TODO: Incorporate other possible changes?
else:
state.list = some(list)
proc renderPostUrl(post: Post, thread: Thread): string =
makeUri(fmt"/t/{thread.id}/p/{post.id}")
proc genPost(post: Post, thread: Thread, isLoggedIn: bool): VNode =
result = buildHtml():
tdiv(class="post"):
tdiv(class="post-icon"):
render(post.author, "post-avatar")
tdiv(class="post-main"):
tdiv(class="post-title"):
tdiv(class="post-username"):
text post.author.name
tdiv(class="post-time"):
let title = post.info.creation.fromUnix().local.
format("MMM d, yyyy HH:mm")
a(href=renderPostUrl(post, thread), title=title):
text renderActivity(post.info.creation)
tdiv(class="post-content"):
p(text post.info.content) # TODO: RSTGEN
tdiv(class="post-buttons"):
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")
if isLoggedIn:
tdiv(class="flag-button"):
button(class="btn"):
italic(class="far fa-flag")
tdiv(class="reply-button"):
button(class="btn"):
italic(class="fas fa-reply")
text " Reply"
proc renderPostList*(threadId: int, isLoggedIn: bool): VNode =
if state.status != Http200:
return renderError("Couldn't retrieve posts.")
if state.list.isNone:
ajaxGet(makeUri("posts.json?id=" & $threadId), @[], onPostList)
return buildHtml(tdiv(class="loading loading-lg"))
let list = state.list.get()
result = buildHtml():
section(class="container grid-xl"):
tdiv(class="title"):
p(): text list.thread.topic
render(list.thread.category)
tdiv(class="posts"):
for post in list.posts:
genPost(post, list.thread, isLoggedIn)

View file

@ -1,4 +1,6 @@
import strformat, times, options, json
import strformat, times, options, json, httpcore
import category
type
User* = object
@ -6,10 +8,6 @@ type
avatarUrl*: string
isOnline*: bool
Category* = object
id*: string
color*: string
Thread* = object
id*: int
topic*: string
@ -31,17 +29,19 @@ when defined(js):
include karax/prelude
import karax / [vstyles, kajax, kdom]
import karaxutils
import karaxutils, error
type
State = ref object
list: Option[ThreadList]
loading: bool
status: HttpCode
proc newState(): State =
State(
list: none[ThreadList](),
loading: false
loading: false,
status: Http200
)
var
@ -63,16 +63,20 @@ when defined(js):
button(class="btn btn-link"): text "Categories"
section(class="navbar-section")
proc render*(user: User, class: string): VNode =
result = buildHtml():
figure(class=class):
img(src=user.avatarUrl, title=user.name)
if user.isOnline:
italic(class="avatar-presense online")
proc genUserAvatars(users: seq[User]): VNode =
result = buildHtml(td):
for user in users:
figure(class="avatar avatar-sm"):
img(src=user.avatarUrl, title=user.name)
if user.isOnline:
italic(class="avatar-presense online")
render(user, "avatar avatar-sm")
text " "
proc renderActivity(activity: int64): string =
proc renderActivity*(activity: int64): string =
let currentTime = getTime()
let activityTime = fromUnix(activity)
let duration = currentTime - activityTime
@ -97,12 +101,7 @@ when defined(js):
italic(class="fas fa-lock fa-xs")
a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic
td():
if thread.category.id.len > 0:
tdiv(class="triangle",
style=style(
(StyleAttr.borderBottom, kstring"0.6rem solid " & thread.category.color)
)):
text thread.category.id
render(thread.category)
genUserAvatars(thread.users)
td(): text $thread.replies
td(class=class({
@ -119,6 +118,9 @@ when defined(js):
proc onThreadList(httpStatus: int, response: kstring) =
state.loading = false
state.status = httpStatus.HttpCode
if state.status != Http200: return
let parsed = parseJson($response)
let list = to(parsed, ThreadList)
@ -132,11 +134,14 @@ when defined(js):
proc onLoadMore(ev: Event, n: VNode) =
state.loading = true
let start = state.list.get().threads.len
ajaxGet("threads.json?start=" & $start, @[], onThreadList)
ajaxGet(makeUri("threads.json?start=" & $start), @[], onThreadList)
proc genThreadList(): VNode =
if state.status != Http200:
return renderError("Couldn't retrieve threads.")
if state.list.isNone:
ajaxGet("threads.json", @[], onThreadList)
ajaxGet(makeUri("threads.json"), @[], onThreadList)
return buildHtml(tdiv(class="loading loading-lg"))

View file

@ -15,6 +15,11 @@ proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
discard
if x in validRange: value = x
proc getInt*(s: string, default = 0): int =
## Safely parses an int and returns it.
result = default
parseInt(s, result, 0..1_000_000_000)
type
Config* = object
smtpAddress: string