This is a trickier issue than it appears at first.
The obvious solution is to just check the date of the last posted message and compare it with, say, the last time the user logged in. But in practice this doesn't work, because you want to view a post and have it removed from your "New" list right away, not at some later date. You actually have to bite the bullet and store a list of posts that you have read or unread. I chose "read" because by default users don't have anything in that list, which is easier to implement.
The list is stored as a string of post Id numbers, delimited by hyphens. When displaying the list of forum posts, it checks each one to see if the post Id is in your list of read posts, just by doing a (find) search on the string.
That part is easy. But what about when someone adds a new comment to a thread you've read? Ideally you want it to be bumped back to "Unread" status.
The solution I came up with is to get a list of every user's Read list whenever someone posts a comment. Then you loop through it and if you find the Post Id in there, you just snip it out using (replace).
Oh, I also added a "Mark All as Read" button, for people who just want all those pesky "New" icons to go away, RIGHT NOW. It gets a list of every post and adds them all to your Read list.
Is it as efficient as it could be? Will it bog down when you have a million users? I have no idea. I'll cross that bridge when I come to it. For now, it seems to work pretty well.
I've added a small feature that shows the number of views each forum thread or blog post has gained. Each time you view a thread in the forum view or view a complete blog post by clicking on the link, the number is bumped.
Because this is a new feature, all the old views aren't counted. :( So they are starting at zero.
This did require a change to the database, as I didn't already have the column "PostViews" in the Posts table. This can present a problem when people are upgrading from an earlier version of Rockets that doesn't have that column. I've updated setup-rockets.lisp to create this field when making a new database, but people's existing databases won't have it. I'll probably have to add a "update database to latest version" script that will just add the missing columns for people with older versions. One nice thing is that the database doesn't need any values in there. It just needs to be altered to add this missing field.
Welcome to 2013! It's fun living in the future.
I've upgraded the server from Ubuntu 10.04 to Ubuntu 12.04 LTS, so please let me know if there are any hiccups or strange behaviors on the site.
I'll be posting more soon!
I'm now starting to use Rockets for projects at work, so I need a way to create the user database from scratch. Anyone wanting to start using the newLISP on Rockets Blog source code will need to do the same thing. So our desires align!
I've created a simple installation script called setup-rockets.lisp. It needs to be run from the command line:
sudo newlisp setup-rockets.lisp
This will ask you a few questions (database, name, password, etc) and set up a new database for you with the right tables and create an admin user. Obviously you never want this to be run by someone else over the web, so make sure that it can't be viewed over your web server (either by permissions, or by using my Apache .htaccess directive that excludes all files ending in .lisp from being viewed (.lsp files are fine though!)
# Prevent framework source from being accessed
<Files ~ "\.lisp$">
Deny from all
# Prevent database files from being accessed
<Files ~ "\.db$">
Deny from all
chown www-data:www-data DATABASENAME.db
chmod 755 DATABASENAME.db
Is now to post more!
Now comments on blog posts, forum posts, or replies to forum posts will count towards your post count total. This feature wasn't implemented before, so I've gone ahead and retroactively counted everyone's posts (thanks to the COUNT(*) features of SQLite) to ensure not a single post of yours goes uncounted!
Post counts are a fun way to keep track of contributions to an online community. Implementing the +1 bump every time a user posts was quite easy:
; now update the user's postcount! postcount++!!
(set 'UserId Rockets:UserId)
(set 'UserPosts (++ Rockets:UserPosts))
(update-record "Users" UserId UserPosts)
Happy Birthday! Or is it?
It's handy in web applications to have a way to select dates in a rigid format, and to have a little calendar to make it easier for users to select dates. I've incorporated a plugin for Bootstrap that does just that.
Inside a form, just use the following code:
(form-datepicker "Enter your birth date" "date" show-birthdate "dp1")
This will open up a little form box where you can type in a date, or if you click on the date box a calendar pops up.
To see it in action, go to your User Profile page (click on your name on the upper right) and you can set your birthdate there! At some point I'll add a little feature that will send you a Happy Birthday email, just for fun.
When I was implementing this I discovered that SQLite wants dates in a YYYY-MM-DD HH:MM:SS.000 format and this calendar wants it in MM-DD-YYYY format. It's quite easy to convert between the two, but I might add some date functions to Rockets to make this easier. Of course everyone has their own preferred date format, which complicates things still further...
After a fair bit of fiddling, I've gotten avatar uploads working on the newLISP on Rockets blog. Now you can upload your own image.
The code to display a file upload form looks like this:
(displayln "<p>Upload new avatar (all avatars scaled to 64x64 pixels): <form name='FileUpload' action='rockets-avatarupload.lsp' method='POST' enctype='multipart/form-data'><input type='file' id='uploadName' name='uploaded_data' onChange='this.form.textname.value = this.value'><input type='hidden' name='textname'><input type='submit' value='Upload' name='submit'></form>")
(set 'file-name ($POST "filename"))
(set 'file-binary ($POST "binary-data"))
(write-file (string "images/avatars/" file-name) file-binary)
sudo chown www-data:www-data images/avatars
It's not quite like winning an Olympic Gold Medal or anything, but it's a nice feeling.
I was having a very intermittent problem with long posts being truncated. Most posts are short enough that they didn't encounter this problem, but longer posts with code would sometimes just cut off randomly. The weird thing is that if I went back and refreshed the page they would go through. It wasn't predictable at all. Those kind of bugs are the most annoying!
Basically I had some old code that followed a similar technique that Dragonfly used:
(when (> (peek (device)) 0)
(if (and (read (device) post-buffer $MAX_POST_LENGTH) post-buffer) ; grab all post data, put it in variable 'post-buffer'
(parse-get-or-post post-buffer $POST)
(let ((buffer "") (post-buffer ""))
(unless (zero? (peek (device)))
(while (read (device) buffer $MAX_POST_LENGTH)
(write post-buffer buffer))
(parse-get-or-post post-buffer $POST)))
Today I implemented an entirely different way to view posts and comments, the forum view. When you view a blog post, the same post also exists in the forums. In fact, you can click on the "View post in forums" button to toggle the views.
A lot of people would tell you to write this kind of thing using the MVC (model-view-controller) model, or at least use a framework that has a templating system so that you can view the same data differently.
This is a completely valid perspective and I'm not going to argue otherwise. MVC is a well-established design methodology and it works pretty well for a lot of different applications. Templates seem like they could be really useful.
I didn't implement the forum view this way, however. I just had a toggle variable called (forum-view-post) and checked to see if it was true. If it was, then I executed a different code block. The real danger here is code duplication, which is something we always want to avoid.
The thing is, implementing this very different view took only ten lines of code.
(displayln "<h3>" (list-post-data 3) "</h3>")
(set 'header-list '("Author" "Message"))
(set 'post-data (list (string "<img src='images/avatars/" (author-avatar (list-post-data 1)) "'><br>" (author-name (list-post-data 1))) (format-for-web (list-post-data 4))))
(set 'PostId (int (list-post-data 0)))
(set 'post-data (list post-data)) ; okay these two lines of code are duplicated... I can live with it for now
(set 'post-comments (get-record "Comments" PostId))
(if post-comments (begin
(dolist (p post-comments)
(push (list (string "<img src='images/avatars/" (author-avatar (p 2)) "'><br>"(author-name (p 2))) (format-for-web (p 5))) post-data -1)) ; add each comment to the thread
(display-table header-list post-data "striped")
Blogs and forums are two different things, but they don't have to be.
I'm adding a forum to the Rockets blog, where any registered user will be able to post on any topic. Each new blog post will appear on the forum, and any comments added in the forum will be reflected in the blog itself. But the inverse is not true-- any new forum post will not appear on the blog. This allows other users to start discussions and interact with each other without interrupting the blog itself.
The way I do this is to add a new column called "PostType" that can be either "Blog post" or "Forum post". The former will be retrieved by the main page, while the latter is retrieved by the forum page.
I'm also using my new (display-table) function to display the forum posts.