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;
$popular-color: darken($super-popular-color, 25%);
$views-color: #545d70;
$threads-meta-color: #545d70;
.super-popular-text {
color: $super-popular-color;
@ -156,7 +156,7 @@ $views-color: #545d70;
}
.views-text {
color: $views-color;
color: $threads-meta-color;
}
.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 {
@extend .grid-md;
@extend .container;

View file

@ -29,8 +29,7 @@ type
Session = object of RootObj
userName, userPass, email: string
rank: Rank
TPost = tuple[subject, content: string]
previousVisitAt: int64
TForumData = ref object of Session
req: Request
@ -137,9 +136,34 @@ proc checkLoggedIn(c: TForumData) =
c.email = ||row[1]
c.rank = parseEnum[Rank](||row[2])
# Update lastOnline
db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?",
c.userid)
# In order to handle the "last visit" line appropriately, i.e.
# it shouldn't disappear after a refresh, we need to manage a
# 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:
warn("SID not found in sessions. Assuming logged out.")
@ -207,7 +231,7 @@ proc verifyIdentHash(
c: TForumData, name: string, epoch: int64, ident: string
) =
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)
if row[0] == "":
raise newForumError("User doesn't exist.", @["nick"])
@ -261,8 +285,9 @@ proc selectUser(userRow: seq[string], avatarSize: int=80): User =
name: userRow[0],
avatarUrl: userRow[1].getGravatarUrl(avatarSize),
lastOnline: userRow[2].parseInt,
rank: parseEnum[Rank](userRow[3]),
isDeleted: userRow[4] == "1"
previousVisitAt: userRow[3]. parseInt,
rank: parseEnum[Rank](userRow[4]),
isDeleted: userRow[5] == "1"
)
# Don't give data about a deleted user.
@ -276,7 +301,7 @@ proc selectPost(postRow: seq[string], skippedPosts: seq[int],
return Post(
id: postRow[0].parseInt,
replyingTo: replyingTo,
author: selectUser(postRow[5..9]),
author: selectUser(postRow[5..10]),
likes: likes,
seen: false, # TODO:
history: history,
@ -292,7 +317,8 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] =
const replyingToQuery = sql"""
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,
t.name
from post p, person u, thread t
@ -307,7 +333,7 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] =
topic: row[^1],
threadId: row[2].parseInt(),
postId: row[0].parseInt(),
author: some(selectUser(row[3..7]))
author: some(selectUser(row[3..8]))
))
proc selectHistory(postId: int): seq[PostInfo] =
@ -326,7 +352,8 @@ proc selectHistory(postId: int): seq[PostInfo] =
proc selectLikes(postId: int): seq[User] =
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
from like h, person u
where h.post = ? and h.author = u.id
@ -340,7 +367,8 @@ proc selectLikes(postId: int): seq[User] =
proc selectThreadAuthor(threadId: int): User =
const authorQuery =
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 (
select author from post
where thread = ?
@ -353,12 +381,12 @@ proc selectThreadAuthor(threadId: int): User =
proc selectThread(threadRow: seq[string]): Thread =
const postsQuery =
sql"""select count(*), strftime('%s', creation) from post
where thread = ?
order by creation asc limit 1;"""
sql"""select count(*), min(strftime('%s', creation)) from post
where thread = ?;"""
const usersListQuery =
sql"""
select name, email, strftime('%s', lastOnline), status, u.isDeleted,
select name, email, strftime('%s', lastOnline),
strftime('%s', previousVisitAt), status, u.isDeleted,
count(*)
from person u, post p where p.author = u.id and p.thread = ?
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 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):
let thread = selectThread(data)
list.threads.add(thread)
@ -769,7 +797,8 @@ routes:
sql(
"""select p.id, p.content, strftime('%s', p.creation), p.author,
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
from post p, person u
where u.id = p.author and p.thread = ? and p.isDeleted = 0
@ -815,7 +844,8 @@ routes:
let postsQuery = sql("""
select p.id, p.content, strftime('%s', p.creation), p.author,
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
from post p, person u
where u.id = p.author and p.id in ($#)
@ -883,7 +913,8 @@ routes:
""" % postsFrom)
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
from person
where name = ? and isDeleted = 0
@ -994,6 +1025,7 @@ routes:
name: c.username,
avatarUrl: c.email.getGravatarUrl(),
lastOnline: getTime().toUnix(),
previousVisitAt: c.previousVisitAt,
rank: c.rank
))
else:
@ -1398,7 +1430,7 @@ routes:
postId: rowFT[2].parseInt(),
postContent: content,
creation: rowFT[4].parseInt(),
author: selectUser(rowFT[5 .. 9]),
author: selectUser(rowFT[5 .. 10]),
)
)

View file

@ -18,7 +18,6 @@ type
ThreadList* = ref object
threads*: seq[Thread]
lastVisit*: int64 ## Unix timestamp
moreCount*: int ## How many more threads are left
proc isModerated*(thread: Thread): bool =
@ -108,6 +107,7 @@ when defined(js):
return $duration.seconds & "s"
proc genThread(thread: Thread, isNew: bool, noBorder: bool): VNode =
let isOld = (getTime() - thread.creation.fromUnix).weeks > 2
result = buildHtml():
tr(class=class({"no-border": noBorder})):
td(class="thread-title"):
@ -136,7 +136,15 @@ when defined(js):
text fmt"{thread.views/1000:.1f}k"
else:
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)
proc onThreadList(httpStatus: int, response: kstring) =
@ -150,7 +158,6 @@ when defined(js):
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)
@ -159,6 +166,29 @@ when defined(js):
let start = state.list.get().threads.len
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 =
if state.status != Http200:
return renderError("Couldn't retrieve threads.", state.status)
@ -187,13 +217,11 @@ when defined(js):
let thread = list.threads[i]
if not visibleTo(thread, currentUser): continue
let isLastVisit =
i+1 < list.threads.len and
list.threads[i].activity < list.lastVisit
let isNew = thread.creation < list.lastVisit
let isLastThread = i+1 == list.threads.len
let (isLastUnseen, isNew) = getInfo(list.threads, i, currentUser)
genThread(thread, isNew,
noBorder=isLastVisit or i+1 == list.threads.len)
if isLastVisit:
noBorder=isLastUnseen or isLastThread)
if isLastUnseen:
tr(class="last-visit-separator"):
td(colspan="6"):
span(text "last visit")

View file

@ -19,6 +19,7 @@ type
name*: string
avatarUrl*: string
lastOnline*: int64
previousVisitAt*: int64 ## Tracks the "last visit" line position
rank*: Rank
isDeleted*: bool

View file

@ -87,6 +87,7 @@ proc initialiseDb(admin: tuple[username, password, email: string],
salt varbin(128) not null,
status varchar(30) not null,
lastOnline timestamp not null default (DATETIME('now')),
previousVisitAt timestamp not null default (DATETIME('now')),
isDeleted boolean not null default 0,
needsPasswordReset boolean not null default 0
);""" % [userNameType, passwordType, emailType]), [])