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;
|
||||
$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;
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ type
|
|||
name*: string
|
||||
avatarUrl*: string
|
||||
lastOnline*: int64
|
||||
previousVisitAt*: int64 ## Tracks the "last visit" line position
|
||||
rank*: Rank
|
||||
isDeleted*: bool
|
||||
|
||||
|
|
|
|||
|
|
@ -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]), [])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue