Implements proper "last visit" line and time highlighting.

This commit is contained in:
Dominik Picheta 2018-05-22 20:06:58 +01:00
commit 568e910040
5 changed files with 108 additions and 33 deletions

View file

@ -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;

View file

@ -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]),
) )
) )

View file

@ -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")

View file

@ -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

View file

@ -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]), [])