Implements thread list in Karax and backend.

This commit is contained in:
Dominik Picheta 2018-05-09 19:02:18 +01:00
commit 8910e55ad1
7 changed files with 276 additions and 57 deletions

View file

@ -1,6 +1,6 @@
#? stdtmpl | standard
#
#template `%`(idx: untyped): untyped =
#template `!`(idx: untyped): untyped =
# row[idx]
#end template
#
@ -47,15 +47,15 @@
<div>
<div class="topic">
<div>
<a href="${c.genThreadUrl(threadid = %threadid)}"
title="${xmlEncode(%name)}">${xmlEncode(%name)}</a>
${genPagenumLocalNav(c, (%threadid).parseInt)}
<a href="${c.genThreadUrl(threadid = !threadid)}"
title="${xmlEncode(!name)}">${xmlEncode(!name)}</a>
${genPagenumLocalNav(c, (!threadid).parseInt)}
</div>
</div>
#let users = getAllRows(db,
# sql("select distinct name, email from person where id in " &
# "(select author from post where thread = ?)"), %threadId)
# "(select author from post where thread = ?)"), !threadId)
<div class="users">
<div>
#for i in 0 .. min(6, users.len-1):
@ -66,19 +66,19 @@
#let latestReplyAuthor = getValue(db, sql("select name from person where id = " &
# "(select author from post where id = " &
# "(select max(id) from post where thread = ?))"), %threadId)
# "(select max(id) from post where thread = ?))"), !threadId)
#let replyProfileUrl = c.req.makeUri("profile/", false) &
# xmlEncode(latestReplyAuthor)
# let posts = getValue(db, sql"select count(*) from post where thread = ?", %threadId)
# let posts = getValue(db, sql"select count(*) from post where thread = ?", !threadId)
<div class="detail">
<div><div title="Views">${xmlEncode(%views)}</div></div>
<div><div title="Views">${xmlEncode(!views)}</div></div>
<div><div title="Posts">$posts</div></div>
</div>
#let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " &
# "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId)
# "(select creation from post where id = (select max(id) from post where thread = ?)))"), !threadId)
#let timeStr = formatTimestamp(latestReplyDate.parseInt())
<div class="activity">
<div>
@ -150,28 +150,28 @@
<div id="talk-thread">
# for row in posts:
# inc(count)
<a name="${%postId}"></a>
<div id="${%postId}">
<a name="${!postId}"></a>
<div id="${!postId}">
<div class="author">
<div>
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName)
<div class="avatar">${genGravatar(%userEmail)}</div>
<a class="name" href="$profileUrl">${xmlEncode(%userName)}</a>
#if c.userId == %postAuthor and c.currentPost.subject.len == 0:
<hr/><a href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(!userName)
<div class="avatar">${genGravatar(!userEmail)}</div>
<a class="name" href="$profileUrl">${xmlEncode(!userName)}</a>
#if c.userId == !postAuthor and c.currentPost.subject.len == 0:
<hr/><a href="${c.genThreadUrl(!postId, "edit")}">Edit post</a>
#elif c.rank >= Moderator and c.currentPost.subject.len == 0:
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
<hr/><a style="color: red;" href="${c.genThreadUrl(!postId, "edit")}">Edit post</a>
#end if
</div>
</div>
<div class="topic">
<div>
#try:
${(%postContent).rstToHtml}
${(!postContent).rstToHtml}
#except EParseError:
# c.errorMsg = getCurrentExceptionMsg()
#end
<span class="date"><a href="${c.genThreadUrl(%postId, "", $threadId, $(c.pageNum))}">${xmlEncode(%postCreation)}</a></span>
<span class="date"><a href="${c.genThreadUrl(!postId, "", $threadId, $(c.pageNum))}">${xmlEncode(!postCreation)}</a></span>
</div>
</div>
</div>
@ -421,43 +421,43 @@
<div id="talk-thread" class="searchResults">
# for row in results():
# inc(count)
# let isThread = %what == "0"
# let isThread = !what == "0"
# inc(whCount[isThread])
# let postUrl = c.genThreadUrl(%postId,"",%threadId,"")
# let threadUrl = c.genThreadUrl("","",%threadId)
# let postUrl = c.genThreadUrl(!postId,"",!threadId,"")
# let threadUrl = c.genThreadUrl("","",!threadId)
# var headersDiffer = false
<div>
<div class="author">
<div>
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName)
<div><a href="$profileUrl">${genGravatar(%userEmail, 40)}</a></div>
<div style="padding: 8px 0"><a href="$profileUrl">${xmlEncode(%userName)}</a></div>
#if c.userId == %postAuthor and c.currentPost.subject.len == 0:
<hr/><a href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(!userName)
<div><a href="$profileUrl">${genGravatar(!userEmail, 40)}</a></div>
<div style="padding: 8px 0"><a href="$profileUrl">${xmlEncode(!userName)}</a></div>
#if c.userId == !postAuthor and c.currentPost.subject.len == 0:
<hr/><a href="${c.genThreadUrl(!postId, "edit", !threadId)}">Edit post</a>
#elif c.rank >= Moderator and c.currentPost.subject.len == 0:
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
<hr/><a style="color: red;" href="${c.genThreadUrl(!postId, "edit", !threadId)}">Edit post</a>
#end if
</div>
</div>
<div class="topic">
<div>
#if %postHeader != "":
#if !postHeader != "":
<div class="postTitle">
<span class="titleHeader">Post:</span>
<a href="${postUrl}">
<span>${%postHeader}</span>
<span>${!postHeader}</span>
</a>
</div>
#end if
#if not isThread:
#try:
${(%postContent).rstToHtml}
${(!postContent).rstToHtml}
#except EParseError:
# c.errorMsg = getCurrentExceptionMsg()
${xmlEncode(%postContent)}
${xmlEncode(!postContent)}
#end
#end if
<span class="date">${xmlEncode(%postCreation)}</span>
<span class="date">${xmlEncode(!postCreation)}</span>
</div>
</div>
</div>

View file

@ -9,7 +9,9 @@
import
os, strutils, times, md5, strtabs, cgi, math, db_sqlite,
scgi, jester, asyncdispatch, asyncnet, cache, sequtils,
parseutils, utils, random, rst, ranks, recaptcha
parseutils, utils, random, rst, ranks, recaptcha, json
import redesign/threadlist except User
when not defined(windows):
import bcrypt # TODO
@ -1024,6 +1026,66 @@ routes:
additionalHeaders = genRSSHeaders(c), showRssLinks = true)
resp data
get "/karax.html":
resp readFile("redesign/karax.html")
get "/nimforum.css":
resp readFile("redesign/nimforum.css"), "text/css"
get "/nimcache/forum.js":
resp readFile("redesign/nimcache/forum.js"), "application/javascript"
get "/images/crown.png":
resp readFile("redesign/images/crown.png"), "image/png"
get "/threads.json":
var
start = 0
count = 30
parseInt(@"start", start, 0..1_000_000)
parseInt(@"count", start, 0..1_000_000)
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 = ?);"""
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
))
list.threads.add(thread)
resp $(%list), "application/json"
get "/threadActivity.xml":
createTFD()
c.isThreadsList = true

View file

@ -207,20 +207,20 @@
<updated>${recent}</updated>
# for row in rows(db, query, 10):
<entry>
<title>${xmlEncode(%name)}</title>
<id>urn:entry:${%threadid}</id>
# let url = c.genThreadUrl(threadid = %threadid,
# pageNum = $(ceil(parseInt(%postCount) / PostsPerPage).int)) &
# "#" & %postId
<title>${xmlEncode(!name)}</title>
<id>urn:entry:${!threadid}</id>
# let url = c.genThreadUrl(threadid = !threadid,
# pageNum = $(ceil(parseInt(!postCount) / PostsPerPage).int)) &
# "#" & !postId
<link rel="alternate" type="text/html"
href="${c.req.makeUri(url)}"/>
<published>${%threadDate}</published>
<updated>${%threadDate}</updated>
<author><name>${xmlEncode(%postAuthor)}</name></author>
<published>${!threadDate}</published>
<updated>${!threadDate}</updated>
<author><name>${xmlEncode(!postAuthor)}</name></author>
<content type="html"
>Posts ${%postCount}, ${xmlEncode(%postAuthor)} said:
>Posts ${!postCount}, ${xmlEncode(!postAuthor)} said:
&lt;p&gt;
${xmlEncode(rstToHtml(%postContent))}</content>
${xmlEncode(rstToHtml(!postContent))}</content>
</entry>
# end for
</feed>
@ -256,20 +256,20 @@ ${xmlEncode(rstToHtml(%postContent))}</content>
<updated>${recent}</updated>
# for row in rows(db, query, 10):
<entry>
<title>${xmlEncode(%postHeader)}</title>
<id>urn:entry:${%postId}</id>
# let url = c.genThreadUrl(threadid = %postThread,
# pageNum = $(ceil(parseInt(%postPosition) / PostsPerPage).int)) &
# "#" & %postId
<title>${xmlEncode(!postHeader)}</title>
<id>urn:entry:${!postId}</id>
# let url = c.genThreadUrl(threadid = !postThread,
# pageNum = $(ceil(parseInt(!postPosition) / PostsPerPage).int)) &
# "#" & !postId
<link rel="alternate" type="text/html"
href="${c.req.makeUri(url)}"/>
<published>${%postRssDate}</published>
<updated>${%postRssDate}</updated>
<author><name>${xmlEncode(%postAuthor)}</name></author>
<published>${!postRssDate}</published>
<updated>${!postRssDate}</updated>
<author><name>${xmlEncode(!postAuthor)}</name></author>
<content type="html"
>On ${xmlEncode(%postHumanDate)}, ${xmlEncode(%postAuthor)} said:
>On ${xmlEncode(!postHumanDate)}, ${xmlEncode(!postAuthor)} said:
&lt;p&gt;
${xmlEncode(rstToHtml(%postContent))}</content>
${xmlEncode(rstToHtml(!postContent))}</content>
</entry>
# end for
</feed>

View file

@ -1,5 +1,21 @@
include karax/prelude
import strformat, times, options, json
include karax/prelude
import karax / [vstyles, kajax]
import threadlist, karaxutils
type
State = ref object
list: Option[ThreadList]
proc newState(): State =
State(
list: none[ThreadList]()
)
const
baseUrl = "http://localhost:5000/"
proc genHeader(): VNode =
result = buildHtml(header(id="main-navbar")):
@ -34,9 +50,29 @@ proc genTopButtons(): VNode =
section(class="navbar-section")
proc createDom(): VNode =
var state = newState()
proc onThreadList(httpStatus: int, response: kstring) =
let parsed = parseJson($response)
let list = to(parsed, ThreadList)
if state.list.isSome:
state.list.get().threads.add(list.threads)
state.list.get().moreCount = list.moreCount
state.list.get().lastVisit = list.lastVisit
else:
state.list = some(list)
proc render(): VNode =
if state.list.isNone:
ajaxGet(baseUrl & "threads.json", @[], onThreadList)
result = buildHtml(tdiv()):
genHeader()
genTopButtons()
if state.list.isNone:
tdiv(class="loading loading-lg")
else:
genThreadList(state.list.get())
setRenderer createDom
setRenderer render

1
redesign/forum.nim.cfg Normal file
View file

@ -0,0 +1 @@
-d:js

5
redesign/karaxutils.nim Normal file
View file

@ -0,0 +1,5 @@
proc class*(classes: varargs[tuple[name: string, present: bool]],
defaultClasses: string = ""): string =
result = defaultClasses & " "
for class in classes:
if class.present: result.add(class.name & " ")

115
redesign/threadlist.nim Normal file
View file

@ -0,0 +1,115 @@
import strformat, times
type
User* = object
name*: string
avatarUrl*: string
isOnline*: bool
Category* = object
id*: string
color*: string
Thread* = object
id*: int
topic*: string
category*: Category
users*: seq[User]
replies*: int
views*: int
activity*: int64 ## Unix timestamp
creation*: int64 ## Unix timestamp
isLocked*: bool
isSolved*: bool
ThreadList* = ref object
threads*: seq[Thread]
lastVisit*: int64 ## Unix timestamp
moreCount*: int ## How many more threads are left
when defined(js):
include karax/prelude
import karax / [vstyles]
import karaxutils
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")
proc renderActivity(activity: int64): string =
let currentTime = getTime()
let activityTime = fromUnix(activity)
let duration = currentTime - activityTime
if duration.days > 300:
return activityTime.local().format("MMM yyyy")
elif duration.days > 30 and duration.days < 300:
return activityTime.local().format("MMM dd")
elif duration.days != 0:
return $duration.days & "d"
elif duration.hours != 0:
return $duration.hours & "h"
elif duration.minutes != 0:
return $duration.minutes & "m"
else:
return $duration.seconds & "s"
proc genThread(thread: Thread, isNew: bool, noBorder: bool): VNode =
result = buildHtml():
tr(class=class({"no-border": noBorder})):
td():
if thread.isLocked:
italic(class="fas fa-lock fa-xs")
text thread.topic
td():
tdiv(class="triangle",
style=style(
(StyleAttr.borderBottom, kstring"0.6rem solid " & thread.category.color)
)):
text thread.category.id
genUserAvatars(thread.users)
td(): text $thread.replies
td(class=class({
"views-text": thread.views < 999,
"popular-text": thread.views > 999 and thread.views < 5000,
"super-popular-text": thread.views > 5000
})):
if thread.views > 999:
text fmt"{thread.views/1000:.1f}"
else:
text $thread.views
td(class=class({"text-success": isNew, "text-gray": not isNew})): # TODO: Colors.
text renderActivity(thread.activity)
proc genThreadList*(list: ThreadList): VNode =
result = buildHtml():
section(class="container grid-xl"): # TODO: Rename to `.thread-list`.
table(class="table"):
thead():
tr:
th(text "Topic")
th(text "Category")
th(text "Users")
th(text "Replies")
th(text "Views")
th(text "Activity")
tbody():
for i in 0 ..< list.threads.len:
let thread = list.threads[i]
let isLastVisit =
i+1 < list.threads.len and list.threads[i].activity < list.lastVisit
let isNew = thread.creation < list.lastVisit
genThread(thread, isNew, noBorder=isLastVisit)
if isLastVisit:
tr(class="last-visit-separator"):
td(colspan="6"):
span(text "last visit")
if list.moreCount > 0:
tr(class="load-more-separator"):
td(colspan="6"):
span(text "load more threads")