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 parseutils, utils, random, rst, ranks, recaptcha, json
import redesign/threadlist except User import redesign/threadlist except User
import redesign/[category, postlist]
when not defined(windows): when not defined(windows):
import bcrypt # TODO import bcrypt # TODO
@ -1013,6 +1014,47 @@ template createTFD() =
if request.cookies.len > 0: if request.cookies.len > 0:
checkLoggedIn(c) 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() initialise()
@ -1037,56 +1079,71 @@ routes:
get "/karax/threads.json": get "/karax/threads.json":
var var
start = 0 start = getInt(@"start", 0)
count = 30 count = getInt(@"count", 30)
parseInt(@"start", start, 0..1_000_000)
parseInt(@"count", start, 0..1_000_000)
const threadsQuery = const threadsQuery =
sql"""select id, name, views, strftime('%s', modified) from thread sql"""select id, name, views, strftime('%s', modified) from thread
order by modified desc limit ?, ?;""" order by modified desc limit ?, ?;""" # TODO: Moderation
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 thrCount = getValue(db, sql"select count(*) from thread;").parseInt() let thrCount = getValue(db, sql"select count(*) from thread;").parseInt()
let moreCount = max(0, thrCount - (start + count)) let moreCount = max(0, thrCount - (start + count))
var list = ThreadList(threads: @[], lastVisit: 0, moreCount: moreCount) var list = ThreadList(threads: @[], lastVisit: 0, moreCount: moreCount)
for data in getAllRows(db, threadsQuery, start, count): for data in getAllRows(db, threadsQuery, start, count):
let posts = getRow(db, postsQuery, data[0]) let thread = selectThread(data)
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
))
list.threads.add(thread) list.threads.add(thread)
resp $(%list), "application/json" 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": get "/threadActivity.xml":
createTFD() createTFD()
c.isThreadsList = true 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 include karax/prelude
import threadlist, karaxutils import threadlist, postlist, karaxutils
type type
State = ref object State = ref object
@ -44,7 +44,7 @@ proc render(): VNode =
result = buildHtml(tdiv()): result = buildHtml(tdiv()):
genHeader() genHeader()
if "/t/" in state.url.pathname: if "/t/" in state.url.pathname:
text "</thread>" renderPostList(3806, false)
else: else:
renderThreadList() 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 type
User* = object User* = object
@ -6,10 +8,6 @@ type
avatarUrl*: string avatarUrl*: string
isOnline*: bool isOnline*: bool
Category* = object
id*: string
color*: string
Thread* = object Thread* = object
id*: int id*: int
topic*: string topic*: string
@ -31,17 +29,19 @@ when defined(js):
include karax/prelude include karax/prelude
import karax / [vstyles, kajax, kdom] import karax / [vstyles, kajax, kdom]
import karaxutils import karaxutils, error
type type
State = ref object State = ref object
list: Option[ThreadList] list: Option[ThreadList]
loading: bool loading: bool
status: HttpCode
proc newState(): State = proc newState(): State =
State( State(
list: none[ThreadList](), list: none[ThreadList](),
loading: false loading: false,
status: Http200
) )
var var
@ -63,16 +63,20 @@ when defined(js):
button(class="btn btn-link"): text "Categories" button(class="btn btn-link"): text "Categories"
section(class="navbar-section") 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 = proc genUserAvatars(users: seq[User]): VNode =
result = buildHtml(td): result = buildHtml(td):
for user in users: for user in users:
figure(class="avatar avatar-sm"): render(user, "avatar avatar-sm")
img(src=user.avatarUrl, title=user.name)
if user.isOnline:
italic(class="avatar-presense online")
text " " text " "
proc renderActivity(activity: int64): string = proc renderActivity*(activity: int64): string =
let currentTime = getTime() let currentTime = getTime()
let activityTime = fromUnix(activity) let activityTime = fromUnix(activity)
let duration = currentTime - activityTime let duration = currentTime - activityTime
@ -97,12 +101,7 @@ when defined(js):
italic(class="fas fa-lock fa-xs") italic(class="fas fa-lock fa-xs")
a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic
td(): td():
if thread.category.id.len > 0: render(thread.category)
tdiv(class="triangle",
style=style(
(StyleAttr.borderBottom, kstring"0.6rem solid " & thread.category.color)
)):
text thread.category.id
genUserAvatars(thread.users) genUserAvatars(thread.users)
td(): text $thread.replies td(): text $thread.replies
td(class=class({ td(class=class({
@ -119,6 +118,9 @@ when defined(js):
proc onThreadList(httpStatus: int, response: kstring) = proc onThreadList(httpStatus: int, response: kstring) =
state.loading = false state.loading = false
state.status = httpStatus.HttpCode
if state.status != Http200: return
let parsed = parseJson($response) let parsed = parseJson($response)
let list = to(parsed, ThreadList) let list = to(parsed, ThreadList)
@ -132,11 +134,14 @@ when defined(js):
proc onLoadMore(ev: Event, n: VNode) = proc onLoadMore(ev: Event, n: VNode) =
state.loading = true state.loading = true
let start = state.list.get().threads.len let start = state.list.get().threads.len
ajaxGet("threads.json?start=" & $start, @[], onThreadList) ajaxGet(makeUri("threads.json?start=" & $start), @[], onThreadList)
proc genThreadList(): VNode = proc genThreadList(): VNode =
if state.status != Http200:
return renderError("Couldn't retrieve threads.")
if state.list.isNone: if state.list.isNone:
ajaxGet("threads.json", @[], onThreadList) ajaxGet(makeUri("threads.json"), @[], onThreadList)
return buildHtml(tdiv(class="loading loading-lg")) return buildHtml(tdiv(class="loading loading-lg"))

View file

@ -15,6 +15,11 @@ proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
discard discard
if x in validRange: value = x 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 type
Config* = object Config* = object
smtpAddress: string smtpAddress: string