Implements proper "last visit" line and time highlighting.
This commit is contained in:
parent
e0802d4db0
commit
568e910040
5 changed files with 108 additions and 33 deletions
|
|
@ -145,7 +145,7 @@ $logo-height: $navbar-height - 20px;
|
||||||
|
|
||||||
$super-popular-color: #f86713;
|
$super-popular-color: #f86713;
|
||||||
$popular-color: darken($super-popular-color, 25%);
|
$popular-color: darken($super-popular-color, 25%);
|
||||||
$views-color: #545d70;
|
$threads-meta-color: #545d70;
|
||||||
|
|
||||||
.super-popular-text {
|
.super-popular-text {
|
||||||
color: $super-popular-color;
|
color: $super-popular-color;
|
||||||
|
|
@ -156,7 +156,7 @@ $views-color: #545d70;
|
||||||
}
|
}
|
||||||
|
|
||||||
.views-text {
|
.views-text {
|
||||||
color: $views-color;
|
color: $threads-meta-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-custom {
|
.label-custom {
|
||||||
|
|
@ -235,6 +235,19 @@ $views-color: #545d70;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thread-time {
|
||||||
|
color: $threads-meta-color;
|
||||||
|
|
||||||
|
&.is-new {
|
||||||
|
@extend .text-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-old {
|
||||||
|
@extend .text-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.posts, .about {
|
.posts, .about {
|
||||||
@extend .grid-md;
|
@extend .grid-md;
|
||||||
@extend .container;
|
@extend .container;
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,7 @@ type
|
||||||
Session = object of RootObj
|
Session = object of RootObj
|
||||||
userName, userPass, email: string
|
userName, userPass, email: string
|
||||||
rank: Rank
|
rank: Rank
|
||||||
|
previousVisitAt: int64
|
||||||
TPost = tuple[subject, content: string]
|
|
||||||
|
|
||||||
TForumData = ref object of Session
|
TForumData = ref object of Session
|
||||||
req: Request
|
req: Request
|
||||||
|
|
@ -137,9 +136,34 @@ proc checkLoggedIn(c: TForumData) =
|
||||||
c.email = ||row[1]
|
c.email = ||row[1]
|
||||||
c.rank = parseEnum[Rank](||row[2])
|
c.rank = parseEnum[Rank](||row[2])
|
||||||
|
|
||||||
# Update lastOnline
|
# In order to handle the "last visit" line appropriately, i.e.
|
||||||
db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?",
|
# it shouldn't disappear after a refresh, we need to manage a
|
||||||
c.userid)
|
# special field called `previousVisitAt` appropriately.
|
||||||
|
# That is if a user hasn't been seen for more than an hour (or so), we can
|
||||||
|
# update `previousVisitAt` to the last time they were online.
|
||||||
|
let personRow = getRow(
|
||||||
|
db,
|
||||||
|
sql"""
|
||||||
|
select strftime('%s', lastOnline), strftime('%s', previousVisitAt)
|
||||||
|
from person where id = ?
|
||||||
|
""",
|
||||||
|
c.userid
|
||||||
|
)
|
||||||
|
c.previousVisitAt = personRow[1].parseInt
|
||||||
|
let diff = getTime() - fromUnix(personRow[0].parseInt)
|
||||||
|
if diff.minutes > 30:
|
||||||
|
c.previousVisitAt = personRow[0].parseInt
|
||||||
|
db.exec(
|
||||||
|
sql"""
|
||||||
|
update person set
|
||||||
|
previousVisitAt = lastOnline, lastOnline = DATETIME('now')
|
||||||
|
where id = ?;
|
||||||
|
""",
|
||||||
|
c.userid
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?",
|
||||||
|
c.userid)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
warn("SID not found in sessions. Assuming logged out.")
|
warn("SID not found in sessions. Assuming logged out.")
|
||||||
|
|
@ -207,7 +231,7 @@ proc verifyIdentHash(
|
||||||
c: TForumData, name: string, epoch: int64, ident: string
|
c: TForumData, name: string, epoch: int64, ident: string
|
||||||
) =
|
) =
|
||||||
const query =
|
const query =
|
||||||
sql"select password, salt, strftime('%s', lastOnline) from person where name = ?"
|
sql"select password, salt from person where name = ?"
|
||||||
var row = getRow(db, query, name)
|
var row = getRow(db, query, name)
|
||||||
if row[0] == "":
|
if row[0] == "":
|
||||||
raise newForumError("User doesn't exist.", @["nick"])
|
raise newForumError("User doesn't exist.", @["nick"])
|
||||||
|
|
@ -261,8 +285,9 @@ proc selectUser(userRow: seq[string], avatarSize: int=80): User =
|
||||||
name: userRow[0],
|
name: userRow[0],
|
||||||
avatarUrl: userRow[1].getGravatarUrl(avatarSize),
|
avatarUrl: userRow[1].getGravatarUrl(avatarSize),
|
||||||
lastOnline: userRow[2].parseInt,
|
lastOnline: userRow[2].parseInt,
|
||||||
rank: parseEnum[Rank](userRow[3]),
|
previousVisitAt: userRow[3]. parseInt,
|
||||||
isDeleted: userRow[4] == "1"
|
rank: parseEnum[Rank](userRow[4]),
|
||||||
|
isDeleted: userRow[5] == "1"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Don't give data about a deleted user.
|
# Don't give data about a deleted user.
|
||||||
|
|
@ -276,7 +301,7 @@ proc selectPost(postRow: seq[string], skippedPosts: seq[int],
|
||||||
return Post(
|
return Post(
|
||||||
id: postRow[0].parseInt,
|
id: postRow[0].parseInt,
|
||||||
replyingTo: replyingTo,
|
replyingTo: replyingTo,
|
||||||
author: selectUser(postRow[5..9]),
|
author: selectUser(postRow[5..10]),
|
||||||
likes: likes,
|
likes: likes,
|
||||||
seen: false, # TODO:
|
seen: false, # TODO:
|
||||||
history: history,
|
history: history,
|
||||||
|
|
@ -292,7 +317,8 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] =
|
||||||
|
|
||||||
const replyingToQuery = sql"""
|
const replyingToQuery = sql"""
|
||||||
select p.id, strftime('%s', p.creation), p.thread,
|
select p.id, strftime('%s', p.creation), p.thread,
|
||||||
u.name, u.email, strftime('%s', u.lastOnline), u.status,
|
u.name, u.email, strftime('%s', u.lastOnline),
|
||||||
|
strftime('%s', u.previousVisitAt), u.status,
|
||||||
u.isDeleted,
|
u.isDeleted,
|
||||||
t.name
|
t.name
|
||||||
from post p, person u, thread t
|
from post p, person u, thread t
|
||||||
|
|
@ -307,7 +333,7 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] =
|
||||||
topic: row[^1],
|
topic: row[^1],
|
||||||
threadId: row[2].parseInt(),
|
threadId: row[2].parseInt(),
|
||||||
postId: row[0].parseInt(),
|
postId: row[0].parseInt(),
|
||||||
author: some(selectUser(row[3..7]))
|
author: some(selectUser(row[3..8]))
|
||||||
))
|
))
|
||||||
|
|
||||||
proc selectHistory(postId: int): seq[PostInfo] =
|
proc selectHistory(postId: int): seq[PostInfo] =
|
||||||
|
|
@ -326,7 +352,8 @@ proc selectHistory(postId: int): seq[PostInfo] =
|
||||||
|
|
||||||
proc selectLikes(postId: int): seq[User] =
|
proc selectLikes(postId: int): seq[User] =
|
||||||
const likeQuery = sql"""
|
const likeQuery = sql"""
|
||||||
select u.name, u.email, strftime('%s', u.lastOnline), u.status,
|
select u.name, u.email, strftime('%s', u.lastOnline),
|
||||||
|
strftime('%s', u.previousVisitAt), u.status,
|
||||||
u.isDeleted
|
u.isDeleted
|
||||||
from like h, person u
|
from like h, person u
|
||||||
where h.post = ? and h.author = u.id
|
where h.post = ? and h.author = u.id
|
||||||
|
|
@ -340,7 +367,8 @@ proc selectLikes(postId: int): seq[User] =
|
||||||
proc selectThreadAuthor(threadId: int): User =
|
proc selectThreadAuthor(threadId: int): User =
|
||||||
const authorQuery =
|
const authorQuery =
|
||||||
sql"""
|
sql"""
|
||||||
select name, email, strftime('%s', lastOnline), status, isDeleted
|
select name, email, strftime('%s', lastOnline),
|
||||||
|
strftime('%s', previousVisitAt), status, isDeleted
|
||||||
from person where id in (
|
from person where id in (
|
||||||
select author from post
|
select author from post
|
||||||
where thread = ?
|
where thread = ?
|
||||||
|
|
@ -353,12 +381,12 @@ proc selectThreadAuthor(threadId: int): User =
|
||||||
|
|
||||||
proc selectThread(threadRow: seq[string]): Thread =
|
proc selectThread(threadRow: seq[string]): Thread =
|
||||||
const postsQuery =
|
const postsQuery =
|
||||||
sql"""select count(*), strftime('%s', creation) from post
|
sql"""select count(*), min(strftime('%s', creation)) from post
|
||||||
where thread = ?
|
where thread = ?;"""
|
||||||
order by creation asc limit 1;"""
|
|
||||||
const usersListQuery =
|
const usersListQuery =
|
||||||
sql"""
|
sql"""
|
||||||
select name, email, strftime('%s', lastOnline), status, u.isDeleted,
|
select name, email, strftime('%s', lastOnline),
|
||||||
|
strftime('%s', previousVisitAt), status, u.isDeleted,
|
||||||
count(*)
|
count(*)
|
||||||
from person u, post p where p.author = u.id and p.thread = ?
|
from person u, post p where p.author = u.id and p.thread = ?
|
||||||
group by name order by count(*) desc limit 5;
|
group by name order by count(*) desc limit 5;
|
||||||
|
|
@ -740,7 +768,7 @@ routes:
|
||||||
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: @[], moreCount: moreCount)
|
||||||
for data in getAllRows(db, threadsQuery, start, count):
|
for data in getAllRows(db, threadsQuery, start, count):
|
||||||
let thread = selectThread(data)
|
let thread = selectThread(data)
|
||||||
list.threads.add(thread)
|
list.threads.add(thread)
|
||||||
|
|
@ -769,7 +797,8 @@ routes:
|
||||||
sql(
|
sql(
|
||||||
"""select p.id, p.content, strftime('%s', p.creation), p.author,
|
"""select p.id, p.content, strftime('%s', p.creation), p.author,
|
||||||
p.replyingTo,
|
p.replyingTo,
|
||||||
u.name, u.email, strftime('%s', u.lastOnline), u.status,
|
u.name, u.email, strftime('%s', u.lastOnline),
|
||||||
|
strftime('%s', u.previousVisitAt), u.status,
|
||||||
u.isDeleted
|
u.isDeleted
|
||||||
from post p, person u
|
from post p, person u
|
||||||
where u.id = p.author and p.thread = ? and p.isDeleted = 0
|
where u.id = p.author and p.thread = ? and p.isDeleted = 0
|
||||||
|
|
@ -815,7 +844,8 @@ routes:
|
||||||
let postsQuery = sql("""
|
let postsQuery = sql("""
|
||||||
select p.id, p.content, strftime('%s', p.creation), p.author,
|
select p.id, p.content, strftime('%s', p.creation), p.author,
|
||||||
p.replyingTo,
|
p.replyingTo,
|
||||||
u.name, u.email, strftime('%s', u.lastOnline), u.status,
|
u.name, u.email, strftime('%s', u.lastOnline),
|
||||||
|
strftime('%s', u.previousVisitAt), u.status,
|
||||||
u.isDeleted
|
u.isDeleted
|
||||||
from post p, person u
|
from post p, person u
|
||||||
where u.id = p.author and p.id in ($#)
|
where u.id = p.author and p.id in ($#)
|
||||||
|
|
@ -883,7 +913,8 @@ routes:
|
||||||
""" % postsFrom)
|
""" % postsFrom)
|
||||||
|
|
||||||
let userQuery = sql("""
|
let userQuery = sql("""
|
||||||
select name, email, strftime('%s', lastOnline), status, isDeleted,
|
select name, email, strftime('%s', lastOnline),
|
||||||
|
strftime('%s', previousVisitAt), status, isDeleted,
|
||||||
strftime('%s', creation), id
|
strftime('%s', creation), id
|
||||||
from person
|
from person
|
||||||
where name = ? and isDeleted = 0
|
where name = ? and isDeleted = 0
|
||||||
|
|
@ -994,6 +1025,7 @@ routes:
|
||||||
name: c.username,
|
name: c.username,
|
||||||
avatarUrl: c.email.getGravatarUrl(),
|
avatarUrl: c.email.getGravatarUrl(),
|
||||||
lastOnline: getTime().toUnix(),
|
lastOnline: getTime().toUnix(),
|
||||||
|
previousVisitAt: c.previousVisitAt,
|
||||||
rank: c.rank
|
rank: c.rank
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
|
|
@ -1398,7 +1430,7 @@ routes:
|
||||||
postId: rowFT[2].parseInt(),
|
postId: rowFT[2].parseInt(),
|
||||||
postContent: content,
|
postContent: content,
|
||||||
creation: rowFT[4].parseInt(),
|
creation: rowFT[4].parseInt(),
|
||||||
author: selectUser(rowFT[5 .. 9]),
|
author: selectUser(rowFT[5 .. 10]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ type
|
||||||
|
|
||||||
ThreadList* = ref object
|
ThreadList* = ref object
|
||||||
threads*: seq[Thread]
|
threads*: seq[Thread]
|
||||||
lastVisit*: int64 ## Unix timestamp
|
|
||||||
moreCount*: int ## How many more threads are left
|
moreCount*: int ## How many more threads are left
|
||||||
|
|
||||||
proc isModerated*(thread: Thread): bool =
|
proc isModerated*(thread: Thread): bool =
|
||||||
|
|
@ -108,6 +107,7 @@ when defined(js):
|
||||||
return $duration.seconds & "s"
|
return $duration.seconds & "s"
|
||||||
|
|
||||||
proc genThread(thread: Thread, isNew: bool, noBorder: bool): VNode =
|
proc genThread(thread: Thread, isNew: bool, noBorder: bool): VNode =
|
||||||
|
let isOld = (getTime() - thread.creation.fromUnix).weeks > 2
|
||||||
result = buildHtml():
|
result = buildHtml():
|
||||||
tr(class=class({"no-border": noBorder})):
|
tr(class=class({"no-border": noBorder})):
|
||||||
td(class="thread-title"):
|
td(class="thread-title"):
|
||||||
|
|
@ -136,7 +136,15 @@ when defined(js):
|
||||||
text fmt"{thread.views/1000:.1f}k"
|
text fmt"{thread.views/1000:.1f}k"
|
||||||
else:
|
else:
|
||||||
text $thread.views
|
text $thread.views
|
||||||
td(class=class({"text-success": isNew, "text-gray": not isNew})): # TODO: Colors.
|
|
||||||
|
let friendlyCreation = thread.creation.fromUnix.local.format(
|
||||||
|
"'First post:' MMM d, yyyy HH:mm'\n'"
|
||||||
|
)
|
||||||
|
let friendlyActivity = thread.activity.fromUnix.local.format(
|
||||||
|
"'Last reply:' MMM d, yyyy HH:mm"
|
||||||
|
)
|
||||||
|
td(class=class({"is-new": isNew, "is-old": isOld}, "thread-time"),
|
||||||
|
title=friendlyCreation & friendlyActivity):
|
||||||
text renderActivity(thread.activity)
|
text renderActivity(thread.activity)
|
||||||
|
|
||||||
proc onThreadList(httpStatus: int, response: kstring) =
|
proc onThreadList(httpStatus: int, response: kstring) =
|
||||||
|
|
@ -150,7 +158,6 @@ when defined(js):
|
||||||
if state.list.isSome:
|
if state.list.isSome:
|
||||||
state.list.get().threads.add(list.threads)
|
state.list.get().threads.add(list.threads)
|
||||||
state.list.get().moreCount = list.moreCount
|
state.list.get().moreCount = list.moreCount
|
||||||
state.list.get().lastVisit = list.lastVisit
|
|
||||||
else:
|
else:
|
||||||
state.list = some(list)
|
state.list = some(list)
|
||||||
|
|
||||||
|
|
@ -159,6 +166,29 @@ when defined(js):
|
||||||
let start = state.list.get().threads.len
|
let start = state.list.get().threads.len
|
||||||
ajaxGet(makeUri("threads.json?start=" & $start), @[], onThreadList)
|
ajaxGet(makeUri("threads.json?start=" & $start), @[], onThreadList)
|
||||||
|
|
||||||
|
proc getInfo(
|
||||||
|
list: seq[Thread], i: int, currentUser: Option[User]
|
||||||
|
): tuple[isLastUnseen, isNew: bool] =
|
||||||
|
## Determines two properties about a thread.
|
||||||
|
##
|
||||||
|
## * isLastUnseen - Whether this is the last thread that had new
|
||||||
|
## activity since the last time the user visited the forum.
|
||||||
|
## * isNew - Whether this thread was created during the time that the
|
||||||
|
# user was absent from the forum.
|
||||||
|
let previousVisitAt =
|
||||||
|
if currentUser.isSome(): currentUser.get().previousVisitAt
|
||||||
|
else: getTime().toUnix
|
||||||
|
assert previousVisitAt != 0
|
||||||
|
|
||||||
|
let thread = list[i]
|
||||||
|
let isUnseen = thread.activity > previousVisitAt
|
||||||
|
let isNextUnseen = i+1 < list.len and list[i+1].activity > previousVisitAt
|
||||||
|
|
||||||
|
return (
|
||||||
|
isLastUnseen: isUnseen and (not isNextUnseen),
|
||||||
|
isNew: thread.creation > previousVisitAt
|
||||||
|
)
|
||||||
|
|
||||||
proc genThreadList(currentUser: Option[User]): VNode =
|
proc genThreadList(currentUser: Option[User]): VNode =
|
||||||
if state.status != Http200:
|
if state.status != Http200:
|
||||||
return renderError("Couldn't retrieve threads.", state.status)
|
return renderError("Couldn't retrieve threads.", state.status)
|
||||||
|
|
@ -187,13 +217,11 @@ when defined(js):
|
||||||
let thread = list.threads[i]
|
let thread = list.threads[i]
|
||||||
if not visibleTo(thread, currentUser): continue
|
if not visibleTo(thread, currentUser): continue
|
||||||
|
|
||||||
let isLastVisit =
|
let isLastThread = i+1 == list.threads.len
|
||||||
i+1 < list.threads.len and
|
let (isLastUnseen, isNew) = getInfo(list.threads, i, currentUser)
|
||||||
list.threads[i].activity < list.lastVisit
|
|
||||||
let isNew = thread.creation < list.lastVisit
|
|
||||||
genThread(thread, isNew,
|
genThread(thread, isNew,
|
||||||
noBorder=isLastVisit or i+1 == list.threads.len)
|
noBorder=isLastUnseen or isLastThread)
|
||||||
if isLastVisit:
|
if isLastUnseen:
|
||||||
tr(class="last-visit-separator"):
|
tr(class="last-visit-separator"):
|
||||||
td(colspan="6"):
|
td(colspan="6"):
|
||||||
span(text "last visit")
|
span(text "last visit")
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ type
|
||||||
name*: string
|
name*: string
|
||||||
avatarUrl*: string
|
avatarUrl*: string
|
||||||
lastOnline*: int64
|
lastOnline*: int64
|
||||||
|
previousVisitAt*: int64 ## Tracks the "last visit" line position
|
||||||
rank*: Rank
|
rank*: Rank
|
||||||
isDeleted*: bool
|
isDeleted*: bool
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ proc initialiseDb(admin: tuple[username, password, email: string],
|
||||||
salt varbin(128) not null,
|
salt varbin(128) not null,
|
||||||
status varchar(30) not null,
|
status varchar(30) not null,
|
||||||
lastOnline timestamp not null default (DATETIME('now')),
|
lastOnline timestamp not null default (DATETIME('now')),
|
||||||
|
previousVisitAt timestamp not null default (DATETIME('now')),
|
||||||
isDeleted boolean not null default 0,
|
isDeleted boolean not null default 0,
|
||||||
needsPasswordReset boolean not null default 0
|
needsPasswordReset boolean not null default 0
|
||||||
);""" % [userNameType, passwordType, emailType]), [])
|
);""" % [userNameType, passwordType, emailType]), [])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue