From 568e91004064e7a3879f3980294191321359b67a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 22 May 2018 20:06:58 +0100 Subject: [PATCH] Implements proper "last visit" line and time highlighting. --- public/css/nimforum.scss | 17 ++++++++- src/forum.nim | 76 ++++++++++++++++++++++++++----------- src/frontend/threadlist.nim | 46 +++++++++++++++++----- src/frontend/user.nim | 1 + src/setup_nimforum.nim | 1 + 5 files changed, 108 insertions(+), 33 deletions(-) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index e733cb8..23a65bc 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -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; diff --git a/src/forum.nim b/src/forum.nim index 56dca58..a7d3e32 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -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]), ) ) diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index 353d54a..09c9dfe 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -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") diff --git a/src/frontend/user.nim b/src/frontend/user.nim index 743d34f..8672953 100644 --- a/src/frontend/user.nim +++ b/src/frontend/user.nim @@ -19,6 +19,7 @@ type name*: string avatarUrl*: string lastOnline*: int64 + previousVisitAt*: int64 ## Tracks the "last visit" line position rank*: Rank isDeleted*: bool diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index c3c9f66..a0c39ff 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -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]), [])