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
|
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
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
|
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
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
|
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"))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue