Implements post list in front end and backend.
This commit is contained in:
parent
b2225dec34
commit
366bdadc90
7 changed files with 283 additions and 59 deletions
131
forum.nim
131
forum.nim
|
|
@ -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
23
redesign/category.nim
Normal 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
17
redesign/error.nim
Normal 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"
|
||||
|
|
@ -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
117
redesign/postlist.nim
Normal 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)
|
||||
|
|
@ -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"))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue