/** All known replies. */
private Set<String> knownReplies = new HashSet<String>();
+ /** All bookmarked posts. */
+ /* synchronize access on itself. */
+ private Set<String> bookmarkedPosts = new HashSet<String>();
+
/** Trusted identities, sorted by own identities. */
private Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
return sones;
}
+ /**
+ * Returns whether the given post is bookmarked.
+ *
+ * @param post
+ * The post to check
+ * @return {@code true} if the given post is bookmarked, {@code false}
+ * otherwise
+ */
+ public boolean isBookmarked(Post post) {
+ return isPostBookmarked(post.getId());
+ }
+
+ /**
+ * Returns whether the post with the given ID is bookmarked.
+ *
+ * @param id
+ * The ID of the post to check
+ * @return {@code true} if the post with the given ID is bookmarked,
+ * {@code false} otherwise
+ */
+ public boolean isPostBookmarked(String id) {
+ synchronized (bookmarkedPosts) {
+ return bookmarkedPosts.contains(id);
+ }
+ }
+
+ /**
+ * Returns all currently known bookmarked posts.
+ *
+ * @return All bookmarked posts
+ */
+ public Set<Post> getBookmarkedPosts() {
+ Set<Post> posts = new HashSet<Post>();
+ synchronized (bookmarkedPosts) {
+ for (String bookmarkedPostId : bookmarkedPosts) {
+ Post post = getPost(bookmarkedPostId, false);
+ if (post != null) {
+ posts.add(post);
+ }
+ }
+ }
+ return posts;
+ }
+
//
// ACTIONS
//
}
/**
+ * Bookmarks the given post.
+ *
+ * @param post
+ * The post to bookmark
+ */
+ public void bookmark(Post post) {
+ bookmarkPost(post.getId());
+ }
+
+ /**
+ * Bookmarks the post with the given ID.
+ *
+ * @param id
+ * The ID of the post to bookmark
+ */
+ public void bookmarkPost(String id) {
+ synchronized (bookmarkedPosts) {
+ bookmarkedPosts.add(id);
+ }
+ }
+
+ /**
+ * Removes the given post from the bookmarks.
+ *
+ * @param post
+ * The post to unbookmark
+ */
+ public void unbookmark(Post post) {
+ unbookmarkPost(post.getId());
+ }
+
+ /**
+ * Removes the post with the given ID from the bookmarks.
+ *
+ * @param id
+ * The ID of the post to unbookmark
+ */
+ public void unbookmarkPost(String id) {
+ synchronized (bookmarkedPosts) {
+ bookmarkedPosts.remove(id);
+ }
+ }
+
+ /**
* Creates a new reply.
*
* @param sone
configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
}
+ /* save bookmarked posts. */
+ int bookmarkedPostCounter = 0;
+ synchronized (bookmarkedPosts) {
+ for (String bookmarkedPostId : bookmarkedPosts) {
+ configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(bookmarkedPostId);
+ }
+ }
+ configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(null);
+
/* now save it. */
configuration.save();
}
}
+ /* load bookmarked posts. */
+ int bookmarkedPostCounter = 0;
+ while (true) {
+ String bookmarkedPostId = configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").getValue(null);
+ if (bookmarkedPostId == null) {
+ break;
+ }
+ synchronized (bookmarkedPosts) {
+ bookmarkedPosts.add(bookmarkedPostId);
+ }
+ }
+
}
/**
return (currentSone != null) && (currentSone.isLikedPostId(post.getId()));
} else if (member.equals("new")) {
return core.isNewPost(post.getId());
+ } else if (member.equals("bookmarked")) {
+ return core.isBookmarked(post);
}
return super.get(templateContext, object, member);
}
--- /dev/null
+/*
+ * Sone - BookmarkPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user bookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BookmarkPage extends SoneTemplatePage {
+
+ /**
+ * @param template
+ * The template to render
+ * @param webInterface
+ * The Sone web interface
+ */
+ public BookmarkPage(Template template, WebInterface webInterface) {
+ super("bookmark.html", template, "Page.Bookmark.Title", webInterface);
+ }
+
+ //
+ // SONETEMPLATEPAGE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ super.processTemplate(request, templateContext);
+ if (request.getMethod() == Method.POST) {
+ String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
+ String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+ webInterface.getCore().bookmarkPost(id);
+ throw new RedirectException(returnPage);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Sone - BookmarksPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user browse all his bookmarked posts.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BookmarksPage extends SoneTemplatePage {
+
+ /**
+ * Creates a new bookmarks page.
+ *
+ * @param template
+ * The template to render
+ * @param webInterface
+ * The Sone web interface
+ */
+ public BookmarksPage(Template template, WebInterface webInterface) {
+ super("bookmarks.html", template, "Page.Bookmarks.Title", webInterface);
+ }
+
+ //
+ // SONETEMPLATEPAGE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ super.processTemplate(request, templateContext);
+ Set<Post> posts = webInterface.getCore().getBookmarkedPosts();
+ List<Post> sortedPosts = new ArrayList<Post>(posts);
+ Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
+ Pagination<Post> pagination = new Pagination<Post>(sortedPosts, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+ templateContext.set("pagination", pagination);
+ templateContext.set("posts", pagination.getItems());
+ }
+
+}
--- /dev/null
+/*
+ * Sone - BookmarkPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user unbookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnbookmarkPage extends SoneTemplatePage {
+
+ /**
+ * @param template
+ * The template to render
+ * @param webInterface
+ * The Sone web interface
+ */
+ public UnbookmarkPage(Template template, WebInterface webInterface) {
+ super("unbookmark.html", template, "Page.Unbookmark.Title", webInterface);
+ }
+
+ //
+ // SONETEMPLATEPAGE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ super.processTemplate(request, templateContext);
+ if (request.getMethod() == Method.POST) {
+ String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
+ String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+ webInterface.getCore().unbookmarkPost(id);
+ throw new RedirectException(returnPage);
+ }
+ }
+
+}
import net.pterodactylus.sone.template.SubstringFilter;
import net.pterodactylus.sone.template.TrustAccessor;
import net.pterodactylus.sone.template.UnknownDateFilter;
+import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
import net.pterodactylus.sone.web.ajax.TrustAjaxPage;
+import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage;
import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
Template createSoneTemplate = TemplateParser.parse(createReader("/templates/createSone.html"));
Template createPostTemplate = TemplateParser.parse(createReader("/templates/createPost.html"));
Template createReplyTemplate = TemplateParser.parse(createReader("/templates/createReply.html"));
+ Template bookmarksTemplate = TemplateParser.parse(createReader("/templates/bookmarks.html"));
Template editProfileTemplate = TemplateParser.parse(createReader("/templates/editProfile.html"));
Template editProfileFieldTemplate = TemplateParser.parse(createReader("/templates/editProfileField.html"));
Template deleteProfileFieldTemplate = TemplateParser.parse(createReader("/templates/deleteProfileField.html"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustPage(emptyTemplate, this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustPage(emptyTemplate, this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkAsKnownPage(emptyTemplate, this)));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarkPage(emptyTemplate, this)));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new UnbookmarkPage(emptyTemplate, this)));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarksPage(bookmarksTemplate, this), "Bookmarks"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteSonePage(deleteSoneTemplate, this), "DeleteSone"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new LoginPage(loginTemplate, this), "Login"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(emptyTemplate, this), "Logout"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new LikeAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikeAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new GetLikesAjaxPage(this)));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarkAjaxPage(this)));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new UnbookmarkAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new MoveProfileFieldAjaxPage(this)));
--- /dev/null
+/*
+ * Sone - BookmarkAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user bookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BookmarkAjaxPage extends JsonPage {
+
+ /**
+ * Creates a new bookmark AJAX page.
+ *
+ * @param webInterface
+ * The Sone web interface
+ */
+ public BookmarkAjaxPage(WebInterface webInterface) {
+ super("bookmark.ajax", webInterface);
+ }
+
+ //
+ // JSONPAGE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected JsonObject createJsonObject(Request request) {
+ String id = request.getHttpRequest().getParam("post", null);
+ if ((id == null) || (id.length() == 0)) {
+ return createErrorJsonObject("invalid-post-id");
+ }
+ webInterface.getCore().bookmarkPost(id);
+ return createSuccessJsonObject();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean requiresLogin() {
+ return false;
+ }
+
+}
--- /dev/null
+/*
+ * Sone - BookmarkAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user unbookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnbookmarkAjaxPage extends JsonPage {
+
+ /**
+ * Creates a new unbookmark AJAX page.
+ *
+ * @param webInterface
+ * The Sone web interface
+ */
+ public UnbookmarkAjaxPage(WebInterface webInterface) {
+ super("unbookmark.ajax", webInterface);
+ }
+
+ //
+ // JSONPAGE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected JsonObject createJsonObject(Request request) {
+ String id = request.getHttpRequest().getParam("post", null);
+ if ((id == null) || (id.length() == 0)) {
+ return createErrorJsonObject("invalid-post-id");
+ }
+ webInterface.getCore().unbookmarkPost(id);
+ return createSuccessJsonObject();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean requiresLogin() {
+ return false;
+ }
+
+}
Navigation.Menu.Item.CreateSone.Tooltip=Create a new Sone
Navigation.Menu.Item.KnownSones.Name=Known Sones
Navigation.Menu.Item.KnownSones.Tooltip=Shows all known Sones
+Navigation.Menu.Item.Bookmarks.Name=Bookmarks
+Navigation.Menu.Item.Bookmarks.Tooltip=Show bookmarked posts
Navigation.Menu.Item.EditProfile.Name=Edit Profile
Navigation.Menu.Item.EditProfile.Tooltip=Edit the Profile of your Sone
Navigation.Menu.Item.DeleteSone.Name=Delete Sone
Page.MarkAsKnown.Title=Mark as Known - Sone
+Page.Bookmark.Title=Bookmark - Sone
+Page.Unbookmark.Title=Remove Bookmark - Sone
+Page.Bookmarks.Title=Bookmarks - Sone
+Page.Bookmarks.Page.Title=Bookmarks
+Page.Bookmarks.Text.NoBookmarks=You don’t have any bookmarks defined right now. You can bookmark posts by clicking the star below the post.
+
Page.NoPermission.Title=Unauthorized Access - Sone
Page.NoPermission.Page.Title=Unauthorized Access
Page.NoPermission.Text.NoPermission=You tried to do something that you do not have sufficient authorization for. Please refrain from such actions in the future or we will be forced to take counter-measures!
View.Sone.Status.Inserting=This Sone is currently being inserted.
View.Post.UnknownAuthor=(unknown)
+View.Post.Bookmarks.PostIsBookmarked=Post is bookmarked, click to remove from bookmarks
+View.Post.Bookmarks.PostIsNotBookmarked=Post is not bookmarked, click to bookmark
View.Post.DeleteLink=Delete
View.Post.SendReply=Post Reply!
View.Post.Reply.DeleteLink=Delete
color: rgb(28, 131, 191);
}
+#sone .post .bookmarks {
+ display: inline;
+ color: rgb(28, 131, 191);
+}
+
+#sone .post .bookmark, #sone .post .unbookmark {
+ display: inline;
+ font: inherit;
+ margin: 0px;
+}
+
#sone .post .time {
display: inline;
color: #666;
display: none;
}
-#sone .post .like.hidden, #sone .post .unlike.hidden, #sone .post .trust.hidden, #sone .post .distrust.hidden, #sone .post .untrust.hidden {
+#sone .post .like.hidden, #sone .post .unlike.hidden, #sone .post .trust.hidden, #sone .post .distrust.hidden, #sone .post .untrust.hidden, #sone .post .bookmark.hidden, #sone .post .unbookmark.hidden {
display: none;
}
-#sone .post .delete button, #sone .post .like button, #sone .post .unlike button, #sone .post .trust button, #sone .post .distrust button, #sone .post .untrust button {
+#sone .post .delete button, #sone .post .like button, #sone .post .unlike button, #sone .post .trust button, #sone .post .distrust button, #sone .post .untrust button, #sone .post .bookmark button, #sone .post .unbookmark button {
border: 0px;
background: none;
padding: 0px;
color: rgb(64, 64, 64);
}
-#sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover, #sone .post .trust button:hover, #sone .post .distrust button:hover, #sone .post .untrust button:hover {
+#sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover, #sone .post .trust button:hover, #sone .post .distrust button:hover, #sone .post .untrust button:hover, #sone .post .bookmark button:hover, #sone .post .unbookmark button:hover {
border: 0px;
background: none;
padding: 0px;
return getSoneElement(element).find(".id").text();
}
+/**
+ * Returns the element of the post with the given ID.
+ *
+ * @param postId
+ * The ID of the post
+ * @returns The element of the post
+ */
+function getPost(postId) {
+ return $("#sone .post#" + postId);
+}
+
function getPostElement(element) {
return $(element).closest(".post");
}
});
}
+/**
+ * Bookmarks the post with the given ID.
+ *
+ * @param postId
+ * The ID of the post to bookmark
+ */
+function bookmarkPost(postId) {
+ (function(postId) {
+ $.getJSON("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+ if ((data != null) && data.success) {
+ getPost(postId).find(".bookmark").toggleClass("hidden", true);
+ getPost(postId).find(".unbookmark").toggleClass("hidden", false);
+ }
+ });
+ })(postId);
+}
+
+/**
+ * Unbookmarks the post with the given ID.
+ *
+ * @param postId
+ * The ID of the post to unbookmark
+ */
+function unbookmarkPost(postId) {
+ $.getJSON("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+ if ((data != null) && data.success) {
+ getPost(postId).find(".bookmark").toggleClass("hidden", false);
+ getPost(postId).find(".unbookmark").toggleClass("hidden", true);
+ }
+ });
+}
+
function updateReplyLikes(replyId) {
$.getJSON("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
if ((data != null) && data.success) {
return false;
});
+ /* convert bookmark/unbookmark buttons to javascript functions. */
+ $(postElement).find(".bookmark").submit(function() {
+ bookmarkPost(getPostId(this));
+ return false;
+ });
+ $(postElement).find(".unbookmark").submit(function() {
+ unbookmarkPost(getPostId(this));
+ return false;
+ });
+
/* add “comment” link. */
addCommentLink(getPostId(postElement), postElement, $(postElement).find(".post-status-line .time"));
--- /dev/null
+<%include include/head.html>
+
+ <div class="page-id hidden">bookmarks</div>
+
+ <h1><%= Page.Bookmarks.Page.Title|l10n|html></h1>
+
+ <div id="posts">
+ <%include include/pagination.html>
+ <%foreach posts post>
+ <%include include/viewPost.html>
+ <%foreachelse>
+ <p><%= Page.Bookmarks.Text.NoBookmarks|l10n|html></p>
+ <%/foreach>
+ <%include include/pagination.html>
+ </div>
+
+<%include include/tail.html>
<div class="text"><% post.text|parse sone=post.sone></div>
</div>
<div class="post-status-line status-line">
+ <div class="bookmarks">
+ <form class="unbookmark<%if !post.bookmarked> hidden<%/if>" action="unbookmark.html" method="post">
+ <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+ <input type="hidden" name="post" value="<% post.id|html>" />
+ <button type="submit" title="<%= View.Post.Bookmarks.PostIsBookmarked|l10n|html>">★</button>
+ </form>
+ <form class="bookmark<%if post.bookmarked> hidden<%/if>" action="bookmark.html" method="post">
+ <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+ <input type="hidden" name="post" value="<% post.id|html>" />
+ <button type="submit" title="<%= View.Post.Bookmarks.PostIsNotBookmarked|l10n|html>">☆</button>
+ </form>
+ </div>
+ <span class='separator'>·</span>
<div class="time"><a href="viewPost.html?post=<% post.id|html>"><% post.time|date format="MMM d, yyyy, HH:mm:ss"></a></div>
<div class="likes<%if post.likes.size|match value=0> hidden<%/if>">
<span class='separator'>·</span>