Implement “like” button.
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 23 Oct 2010 00:30:50 +0000 (02:30 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 23 Oct 2010 00:30:50 +0000 (02:30 +0200)
14 files changed:
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/core/SoneInserter.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/template/PostAccessor.java
src/main/java/net/pterodactylus/sone/web/LikePostPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/UnlikePostPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/insert/sone.xml
src/main/resources/templates/likePost.html [new file with mode: 0644]
src/main/resources/templates/unlikePost.html [new file with mode: 0644]

index 200066f..a4b3569 100644 (file)
@@ -523,6 +523,23 @@ public class Core extends AbstractService {
        }
 
        /**
+        * Gets all Sones that like the given post.
+        *
+        * @param post
+        *            The post to check for
+        * @return All Sones that like the post
+        */
+       public Set<Sone> getLikes(final Post post) {
+               return Filters.filteredSet(getSones(), new Filter<Sone>() {
+
+                       @Override
+                       public boolean filterObject(Sone sone) {
+                               return sone.isLikedPostId(post.getId());
+                       }
+               });
+       }
+
+       /**
         * Deletes the given reply. It is removed from its Sone and from the reply
         * cache.
         *
@@ -645,6 +662,17 @@ public class Core extends AbstractService {
                                        sone.addBlockedSoneId(blockedSoneId);
                                }
 
+                               /* load liked post IDs. */
+                               int likedPostIdCounter = 0;
+                               while (true) {
+                                       String likedPostIdPrefix = sonePrefix + "/LikedPostId." + likedPostIdCounter++;
+                                       String likedPostId = configuration.getStringValue(likedPostIdPrefix + "/ID").getValue(null);
+                                       if (likedPostId == null) {
+                                               break;
+                                       }
+                                       sone.addLikedPostId(likedPostId);
+                               }
+
                                sone.setModificationCounter(modificationCounter);
                                addLocalSone(sone);
                        } catch (MalformedURLException mue1) {
@@ -738,6 +766,14 @@ public class Core extends AbstractService {
                                }
                                configuration.getStringValue(sonePrefix + "/BlockedSone." + blockedSoneCounter + "/ID").setValue(null);
 
+                               /* write all liked posts. */
+                               int likedPostIdCounter = 0;
+                               for (String soneLikedPostId : sone.getLikedPostIds()) {
+                                       String likedPostIdPrefix = sonePrefix + "/LikedPostId." + likedPostIdCounter++;
+                                       configuration.getStringValue(likedPostIdPrefix + "/ID").setValue(soneLikedPostId);
+                               }
+                               configuration.getStringValue(sonePrefix + "/LikedPostId." + likedPostIdCounter + "/ID").setValue(null);
+
                        }
                        /* write null ID as terminator. */
                        configuration.getStringValue("Sone/Sone." + soneId + "/ID").setValue(null);
index 5d40881..4923736 100644 (file)
@@ -276,6 +276,20 @@ public class SoneDownloader extends AbstractService {
                                }
                        }
 
+                       /* parse liked post IDs. */
+                       SimpleXML likePostIdsXml = soneXml.getNode("post-likes");
+                       if (likePostIdsXml == null) {
+                               /* TODO - mark Sone as bad. */
+                               logger.log(Level.WARNING, "Downloaded Sone %s has no known Sones!", new Object[] { sone });
+                               return null;
+                       }
+
+                       Set<String> likedPostIds = new HashSet<String>();
+                       for (SimpleXML likedPostIdXml : likePostIdsXml.getNodes("post-like")) {
+                               String postId = likedPostIdXml.getValue();
+                               likedPostIds.add(postId);
+                       }
+
                        /* parse known Sones. */
                        SimpleXML knownSonesXml = soneXml.getNode("known-sones");
                        if (knownSonesXml == null) {
@@ -309,6 +323,7 @@ public class SoneDownloader extends AbstractService {
                                sone.setProfile(profile);
                                sone.setPosts(posts);
                                sone.setReplies(replies);
+                               sone.setLikePostIds(likedPostIds);
                                sone.setModificationCounter(0);
                        }
 
index cf26ebb..8c67a91 100644 (file)
@@ -182,6 +182,7 @@ public class SoneInserter extends AbstractService {
                        soneProperties.put("posts", new ArrayList<Post>(sone.getPosts()));
                        soneProperties.put("replies", new HashSet<Reply>(sone.getReplies()));
                        soneProperties.put("blockedSoneIds", new HashSet<String>(sone.getBlockedSoneIds()));
+                       soneProperties.put("likedPostIds", new HashSet<String>(sone.getLikedPostIds()));
                }
 
                //
index b38f8be..2e23ed1 100644 (file)
@@ -76,6 +76,9 @@ public class Sone {
        /** The IDs of all blocked Sones. */
        private final Set<String> blockedSoneIds = new HashSet<String>();
 
+       /** The IDs of all liked posts. */
+       private final Set<String> likedPostIds = new HashSet<String>();
+
        /** Modification count. */
        private volatile long modificationCounter = 0;
 
@@ -436,6 +439,69 @@ public class Sone {
        }
 
        /**
+        * Returns the IDs of all liked posts.
+        *
+        * @return All liked posts’ IDs
+        */
+       public Set<String> getLikedPostIds() {
+               return Collections.unmodifiableSet(likedPostIds);
+       }
+
+       /**
+        * Sets the IDs of all liked posts.
+        *
+        * @param likedPostIds
+        *            All liked posts’ IDs
+        * @return This Sone (for method chaining)
+        */
+       public synchronized Sone setLikePostIds(Set<String> likedPostIds) {
+               this.likedPostIds.clear();
+               this.likedPostIds.addAll(likedPostIds);
+               modificationCounter++;
+               return this;
+       }
+
+       /**
+        * Checks whether the given post ID is liked by this Sone.
+        *
+        * @param postId
+        *            The ID of the post
+        * @return {@code true} if this Sone likes the given post, {@code false}
+        *         otherwise
+        */
+       public boolean isLikedPostId(String postId) {
+               return likedPostIds.contains(postId);
+       }
+
+       /**
+        * Adds the given post ID to the list of posts this Sone likes.
+        *
+        * @param postId
+        *            The ID of the post
+        * @return This Sone (for method chaining)
+        */
+       public synchronized Sone addLikedPostId(String postId) {
+               if (likedPostIds.add(postId)) {
+                       modificationCounter++;
+               }
+               return this;
+       }
+
+       /**
+        * Removes the given post ID from the list of posts this Sone likes.
+        *
+        * @param postId
+        *            The ID of the post
+        * @return This Sone (for method chaining)
+        */
+       public synchronized Sone removeLikedPostId(String postId) {
+               if (likedPostIds.remove(postId)) {
+                       modificationCounter++;
+               }
+               return this;
+       }
+
+       /**
         * Returns the modification counter.
         *
         * @return The modification counter
index 9284f0d..2c0b59e 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.template;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.ReflectionAccessor;
 
@@ -51,8 +52,14 @@ public class PostAccessor extends ReflectionAccessor {
         */
        @Override
        public Object get(DataProvider dataProvider, Object object, String member) {
+               Post post = (Post) object;
                if ("replies".equals(member)) {
-                       return core.getReplies((Post) object);
+                       return core.getReplies(post);
+               } else if (member.equals("likes")) {
+                       return core.getLikes(post);
+               } else if (member.equals("liked")) {
+                       Sone currentSone = (Sone) dataProvider.getData("currentSone");
+                       return (currentSone != null) && (currentSone.isLikedPostId(post.getId()));
                }
                return super.get(dataProvider, object, member);
        }
diff --git a/src/main/java/net/pterodactylus/sone/web/LikePostPage.java b/src/main/java/net/pterodactylus/sone/web/LikePostPage.java
new file mode 100644 (file)
index 0000000..991627e
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sone - LikePostPage.java - Copyright © 2010 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.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+
+/**
+ * Page that lets the user like a {@link Post}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LikePostPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “like post” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public LikePostPage(Template template, WebInterface webInterface) {
+               /* TODO */
+               super("likePost.html", template, "Page.LikePost.Title", webInterface);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, Template template) throws RedirectException {
+               super.processTemplate(request, template);
+               if (request.getMethod() == Method.POST) {
+                       String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 64);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       currentSone.addLikedPostId(postId);
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return true;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/UnlikePostPage.java b/src/main/java/net/pterodactylus/sone/web/UnlikePostPage.java
new file mode 100644 (file)
index 0000000..b325b5e
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Sone - UnlikePostPage.java - Copyright © 2010 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.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+
+/**
+ * Page that lets the user unlike a {@link Post}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnlikePostPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “unlike post” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UnlikePostPage(Template template, WebInterface webInterface) {
+               super("unlikePost.html", template, "Page.UnlikePost.Title", webInterface);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, Template template) throws RedirectException {
+               super.processTemplate(request, template);
+               if (request.getMethod() == Method.POST) {
+                       String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 64);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       currentSone.removeLikedPostId(postId);
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return true;
+       }
+
+}
index 4294e64..388e64f 100644 (file)
@@ -197,6 +197,8 @@ public class WebInterface extends AbstractService {
                Template blockSoneTemplate = templateFactory.createTemplate(createReader("/templates/blockSone.html"));
                Template unblockSoneTemplate = templateFactory.createTemplate(createReader("/templates/unblockSone.html"));
                Template viewPostTemplate = templateFactory.createTemplate(createReader("/templates/viewPost.html"));
+               Template likePostTemplate = templateFactory.createTemplate(createReader("/templates/likePost.html"));
+               Template unlikePostTemplate = templateFactory.createTemplate(createReader("/templates/unlikePost.html"));
                Template deletePostTemplate = templateFactory.createTemplate(createReader("/templates/deletePost.html"));
                Template deleteReplyTemplate = templateFactory.createTemplate(createReader("/templates/deleteReply.html"));
                Template followSoneTemplate = templateFactory.createTemplate(createReader("/templates/followSone.html"));
@@ -219,6 +221,8 @@ public class WebInterface extends AbstractService {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new BlockSonePage(blockSoneTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnblockSonePage(unblockSoneTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new ViewPostPage(viewPostTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new LikePostPage(likePostTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikePostPage(unlikePostTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeletePostPage(deletePostTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteReplyPage(deleteReplyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSonePage(followSoneTemplate, this)));
index bce5929..3235622 100644 (file)
@@ -158,6 +158,8 @@ View.Sone.Status.Inserting=This Sone is currently being inserted.
 View.Post.DeleteLink=Delete
 View.Post.SendReply=Post Reply!
 View.Post.Reply.DeleteLink=Delete
+View.Post.LikeLink=Like
+View.Post.UnlikeLink=Unlike
 
 WebInterface.DefaultText.StatusUpdate=What’s on your mind?
 WebInterface.DefaultText.CreateSoneName=The name of your Sone
index 79c91b8..f6628ce 100644 (file)
@@ -143,20 +143,23 @@ textarea {
        color: #666;
 }
 
-#sone .post .delete {
+#sone .post .delete, #sone .post .likes, #sone .post .like, #sone .post .unlike {
        display: inline;
+       font: inherit;
 }
 
-#sone .post .delete button {
-       font: inherit;
+#sone .post .like.hidden, #sone .post .unlike.hidden {
+       display: none;
+}
+
+#sone .post .delete button, #sone .post .like button, #sone .post .unlike button {
        border: 0px;
        background: none;
        padding: 0px;
        color: rgb(28, 131, 191);
 }
 
-#sone .post .delete button:hover {
-       font: inherit;
+#sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover {
        border: 0px;
        background: none;
        padding: 0px;
@@ -164,10 +167,15 @@ textarea {
        cursor: pointer;
 }
 
-#sone .post .delete:before {
+#sone .post .delete:before, #sone .post .likes:before, #sone .post .like:before, #sone .post .unlike:before {
        content: ' ‧ ';
 }
 
+#sone .post .likes span {
+       font: inherit;
+       color: green;
+}
+
 #sone .post .replies {
        clear: both;
        padding-top: 0.2ex;
index 5c9ef15..1f0020c 100644 (file)
@@ -5,6 +5,21 @@
        </div>
        <div class="status-line">
                <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"><span>⬆<span class="like-count"><% post.likes.size></span></span></div>
+               <%ifnull ! currentSone>
+                       <form class="like<%if post.liked> hidden<%/if>" action="likePost.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" value="1"><%= View.Post.LikeLink|l10n|html></button>
+                       </form>
+                       <form class="unlike<%if ! post.liked> hidden<%/if>" action="unlikePost.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" value="1"><%= View.Post.UnlikeLink|l10n|html></button>
+                       </form>
+               <%/if>
                <%if post.sone.current>
                        <form class="delete" action="deletePost.html" method="post">
                                <input type="hidden" name="formPassword" value="<% formPassword|html>" />
index 27b9c20..66032d5 100644 (file)
                <%/foreach>
        </replies>
 
+       <post-likes>
+               <%foreach currentSone.likedPostIds postId>
+               <post-like><% postId|xml></post-like>
+               <%/foreach>
+       </post-likes>
+
        <known-sones>
                <%foreach knownSones sone>
                <known-sone>
diff --git a/src/main/resources/templates/likePost.html b/src/main/resources/templates/likePost.html
new file mode 100644 (file)
index 0000000..196af72
--- /dev/null
@@ -0,0 +1,3 @@
+<%include include/head.html>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/unlikePost.html b/src/main/resources/templates/unlikePost.html
new file mode 100644 (file)
index 0000000..196af72
--- /dev/null
@@ -0,0 +1,3 @@
+<%include include/head.html>
+
+<%include include/tail.html>