Using pushState to navigate from essay list to single essay

This commit is contained in:
moandji.ezana 2012-10-30 09:34:48 +02:00
parent 83ed302667
commit ac4f1331b5
11 changed files with 176 additions and 78 deletions

View file

@ -0,0 +1,16 @@
package com.moandjiezana.essayist.utils;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Tasks {
private final ExecutorService executor = Executors.newCachedThreadPool();
public Future<?> run(Runnable runnable) {
return executor.submit(runnable);
}
}

View file

@ -1,6 +1,7 @@
package com.moandjiezana.tent.essayist;
import com.google.common.base.Throwables;
import com.moandjiezana.tent.client.TentClient;
import com.moandjiezana.tent.client.TentClientAsync;
import com.moandjiezana.tent.client.posts.Post;
import com.moandjiezana.tent.client.posts.PostQuery;
@ -45,4 +46,16 @@ public class Essays {
return posts;
}
public List<Post> getFeed(User user) {
TentClient tentClient = new TentClient(user.getProfile());
tentClient.getAsync().setAccessToken(user.getAccessToken());
tentClient.getAsync().setRegistrationResponse(user.getRegistration());
return tentClient.getPosts(new PostQuery().postTypes(Post.Types.essay("v0.1.0")));
}
public Post get(String essayId) {
return null;
}
}

View file

@ -1,21 +1,15 @@
package com.moandjiezana.tent.essayist;
import com.moandjiezana.tent.client.TentClient;
import com.moandjiezana.essayist.utils.Tasks;
import com.moandjiezana.tent.client.posts.Post;
import com.moandjiezana.tent.client.posts.PostQuery;
import com.moandjiezana.tent.client.users.Profile;
import com.moandjiezana.tent.essayist.auth.Authenticated;
import com.moandjiezana.tent.essayist.tent.Entities;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.inject.Inject;
import javax.inject.Provider;
@ -32,17 +26,18 @@ import org.slf4j.LoggerFactory;
public class MyFeedServlet extends HttpServlet {
private static final Logger LOGGER = LoggerFactory.getLogger(MyFeedServlet.class);
public static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
private final Templates templates;
private Essays essays;
private Users users;
private Provider<EssayistSession> sessions;
private Tasks tasks;
@Inject
public MyFeedServlet(Users users, Essays essays, Provider<EssayistSession> sessions, Templates templates) {
public MyFeedServlet(Users users, Essays essays, Tasks tasks, Provider<EssayistSession> sessions, Templates templates) {
this.users = users;
this.essays = essays;
this.tasks = tasks;
this.sessions = sessions;
this.templates = templates;
}
@ -52,10 +47,7 @@ public class MyFeedServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
User user = sessions.get().getUser();
TentClient tentClient = new TentClient(user.getProfile());
tentClient.getAsync().setAccessToken(user.getAccessToken());
tentClient.getAsync().setRegistrationResponse(user.getRegistration());
List<Post> essays = tentClient.getPosts(new PostQuery().postTypes(Post.Types.essay("v0.1.0")));
List<Post> essaysFeed = essays.getFeed(user);
List<User> allUsers = users.getAll();
final Map<String, Profile> profiles = new ConcurrentHashMap<String, Profile>();
@ -64,49 +56,13 @@ public class MyFeedServlet extends HttpServlet {
profiles.put(aUser.getProfile().getCore().getEntity(), aUser.getProfile());
}
List<Callable<Profile>> missingUsers = new ArrayList<Callable<Profile>>();
int missingUsersCount = 0;
for (Post essay : essays) {
for (Post essay : essaysFeed) {
if (!profiles.containsKey(essay.getEntity())) {
missingUsersCount++;
users.fetch(essay.getEntity());
}
}
final CountDownLatch countDownLatch = new CountDownLatch(missingUsersCount);
for (final Post essay : essays) {
if (profiles.containsKey(essay.getEntity())) {
continue;
}
missingUsers.add(new Callable<Profile>() {
@Override
public Profile call() throws Exception {
try {
TentClient tentClientAsync = new TentClient(essay.getEntity());
Profile profile = tentClientAsync.getProfile();
users.save(new User(profile));
profiles.put(essay.getEntity(), profile);
return profile;
} finally {
countDownLatch.countDown();
}
}
});
}
if (!missingUsers.isEmpty()) {
try {
EXECUTOR.invokeAll(missingUsers);
countDownLatch.await();
} catch (Exception e) {
LOGGER.error("Problem while fetching missing profiles", e);
}
}
templates.read().setEssays(essays).render(resp.getWriter(), profiles);
templates.read().setEssays(essaysFeed).render(resp.getWriter(), profiles);
}
@Override

View file

@ -5,6 +5,8 @@ import com.google.common.io.CharStreams;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.moandjiezana.essayist.utils.Tasks;
import com.moandjiezana.tent.client.TentClient;
import com.moandjiezana.tent.client.apps.RegistrationResponse;
import com.moandjiezana.tent.client.users.Profile;
import com.moandjiezana.tent.oauth.AccessToken;
@ -24,15 +26,21 @@ import org.apache.commons.dbutils.BeanProcessor;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class Users {
private static final Logger LOGGER = LoggerFactory.getLogger(Users.class);
private final QueryRunner queryRunner;
private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
private Tasks tasks;
@Inject
public Users(QueryRunner queryRunner) {
public Users(Tasks tasks, QueryRunner queryRunner) {
this.tasks = tasks;
this.queryRunner = queryRunner;
}
@ -91,6 +99,20 @@ public class Users {
}
}
public void fetch(final String entity) {
tasks.run(new Runnable() {
@Override
public void run() {
try {
Profile updatedProfile = new TentClient(entity).getProfile();
save(new User(updatedProfile));
} catch (Exception e) {
LOGGER.error("Could not fetch Profile", Throwables.getRootCause(e));
}
}
});
}
private <T> T convert(Object value, Class<T> objectClass) {
String s;
if (value instanceof String) {

View file

@ -4,6 +4,7 @@ import com.google.common.base.Splitter;
import com.moandjiezana.tent.client.users.Profile;
import com.moandjiezana.tent.essayist.EssayistSession;
import com.moandjiezana.tent.essayist.User;
import com.moandjiezana.tent.essayist.security.Csrf;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
@ -14,6 +15,7 @@ public class JamonContext {
public final String contextPath;
public final Routes routes;
public final Csrf csrf = new Csrf();
private final HttpServletRequest req;
public final String currentUrl;

View file

@ -17,6 +17,10 @@ public class Routes {
this.req = req;
}
public String assets(String asset) {
return req.getContextPath() + "/assets/" + asset;
}
public String essay(Post essay) {
return req.getContextPath() + "/" + Entities.getForUrl(essay.getEntity()) + "/essay/" + essay.getId();
}

View file

@ -39,6 +39,6 @@ public class Entities {
}
public static String getName(Profile profile, String fallback) {
return profile.getBasic() != null && !Strings.isNullOrEmpty(profile.getBasic().getName()) ? profile.getBasic().getName() : fallback;
return profile != null && profile.getBasic() != null && !Strings.isNullOrEmpty(profile.getBasic().getName()) ? profile.getBasic().getName() : fallback;
}
}

View file

@ -12,7 +12,7 @@ Profile profile;
String active = "My Feed";
</%args>
<&| Layout; active = active; &>
<div class="row">
<div class="row" data-essay="intro">
<div class="span2">
<%if profile.getBasic() != null && profile.getBasic().getAvatarUrl() != null %>
<img src="<% profile.getBasic().getAvatarUrl() %>" />
@ -29,6 +29,6 @@ String active = "My Feed";
</div>
<%for Post essay : essays %>
<& partials/EssayLink; essay = essay; &>
<& partials/EssayLink; essay = essay; profile = profile; showProfile = false; &>
</%for>
</&>

View file

@ -2,6 +2,7 @@
<%import>
com.moandjiezana.tent.essayist.tent.*;
com.moandjiezana.tent.essayist.config.*;
</%import>
<%frag body/>
<%args>
@ -48,7 +49,7 @@ String url;
font-family: 'Alegreya', Georgia, serif; font-size: 1.5em;
}
.essaySummary {
.separator {
border-bottom: 1px solid #eee;
}
</style>
@ -59,6 +60,7 @@ String url;
<![endif]-->
<!--script src="<% jamonContext.contextPath %>/assets/bootstrap-2.1.1/js/bootstrap.min.js"></script-->
<script src="<% jamonContext.routes.assets("essayist.js") %>"></script>
</head>
<body>
@ -90,11 +92,11 @@ String url;
</div>
</div>
<div class="container">
<div id="main" class="container">
<& body &>
<div>
<small>Version: 20121025</small>
</div>
<div class="container">
<small>Version: 20121025</small>
</div>
</body>
</html>

View file

@ -9,30 +9,48 @@ java.util.*;
<%args>
Post essay;
Profile profile = null;
boolean showProfile = true;
</%args>
<%java>
final SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy");
final EssayContent essayContent = essay.getContentAs(EssayContent.class);
String entityForUrl = Entities.getForUrl(essay.getEntity());
String entityName = Entities.getName(profile, essay.getEntity());
String authorPageUrl = jamonContext.contextPath + "/" + entityForUrl + "/essays";
String formattedPublicationDate = dateFormat.format(new Date(essay.getPublishedAt() * 1000));
</%java>
<div class="essaySummary row-fluid">
<%if profile != null %>
<div class="span2">
<%if profile.getBasic() != null && profile.getBasic().getAvatarUrl() != null %>
<img src="<% profile.getBasic().getAvatarUrl() %>" />
</%if>
</div>
</%if>
<div class="span10">
<h3><a href="<% jamonContext.contextPath %>/<% Entities.getForUrl(essay.getEntity()) %>/essay/<% essay.getId() %>"><% essayContent.getTitle() %></a></h3>
<%if profile != null %><a href="<% jamonContext.contextPath %>/<% Entities.getForUrl(essay.getEntity()) %>/essays"><% Entities.getName(profile, essay.getEntity()) %></a></%if> <% dateFormat.format(new Date(essay.getPublishedAt() * 1000)) %>
<p class="bodyText"><% essayContent.getExcerpt() %></p>
<%if jamonContext.getCurrentUser().owns(essay) %>
<div>
<form action="<% jamonContext.contextPath %>/<% Entities.getForUrl(essay.getEntity()) %>/essay/<% essay.getId() %>" method="post">
<button class="btn btn-danger"><i class="icon-trash icon-white"></i> Delete</button>
<input type="hidden" name="_method" value="DELETE" />
</form>
<div data-essay="container" data-essay-author="<% entityForUrl %>" data-essay-id="<% essay.getId() %>">
<div class="row" data-essay="summary" class="separator">
<%if showProfile %>
<div class="span2">
<%if profile != null && profile.getBasic() != null && profile.getBasic().getAvatarUrl() != null %>
<a href="<% authorPageUrl %>"><img src="<% profile.getBasic().getAvatarUrl() %>" style="border: none"/></a>
</%if>
</div>
</%if>
<div class="span10">
<h3><a href="<% jamonContext.contextPath %>/<% entityForUrl %>/essay/<% essay.getId() %>" data-essay="link"><% essayContent.getTitle() %></a></h3>
<%if showProfile %><a href="<% jamonContext.contextPath %>/<% entityForUrl %>/essays"><% entityName %></a></%if> <% formattedPublicationDate %>
<p class="excerpt"><% essayContent.getExcerpt() %></p>
<%if jamonContext.getCurrentUser().owns(essay) %>
<div>
<form action="<% jamonContext.contextPath %>/<% entityForUrl %>/essay/<% essay.getId() %>" method="post">
<button class="btn btn-danger"><i class="icon-trash icon-white"></i> Delete</button>
<input type="hidden" name="_method" value="DELETE" />
</form>
</div>
</%if>
</div>
</div>
<div class="row" data-essay="full" style="display: none">
<div class="span12">
<h1><% essayContent.getTitle() %> <small><a href="<% jamonContext.currentUrl %>" style="text-decoration: none"><% Character.valueOf('\u2693') %></a></small></h1>
<h3><small>by</small> <a href="<% jamonContext.contextPath %>/<% entityForUrl %>/essays"><% entityName %></a> <small><% formattedPublicationDate %></small></h3>
<blockquote class="muted"><% essayContent.getExcerpt() %></blockquote>
<div class="bodyText">
<% jamonContext.csrf.stripScripts(essayContent.getBody()) #n %>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,65 @@
function init() {
var essayContainers = document.querySelectorAll("[data-essay=container]");
var i;
var displaySection = function (section, target) {
var j;
var detailDisplay = section === "essay" ? "block" : "none";
var listDisplay = section === "list" ? "block" : "none";
if (target !== document) {
target.querySelector("[data-essay=summary]").style.display = listDisplay;
target.querySelector("[data-essay=full]").style.display = detailDisplay;
}
for (j = 0; j < essayContainers.length; j++) {
var essayContainer = essayContainers[j];
if (essayContainer !== target) {
essayContainer.style.display = listDisplay;
essayContainer.querySelector("[data-essay=summary]").style.display = listDisplay;
essayContainer.querySelector("[data-essay=full]").style.display = detailDisplay;
}
}
var intro = document.querySelector("[data-essay=intro]");
if (intro !== null) {
intro.style.display = listDisplay;
}
};
var essayClickHandler = function (event) {
if (event.target.dataset.essay !== "link") {
return;
}
event.preventDefault();
event.stopPropagation();
displaySection("essay", event.currentTarget);
history.pushState({ essayId: event.currentTarget.dataset.essayId, essayAuthor: event.currentTarget.dataset.essayAuthor }, "essay title", event.target.href);
return false;
};
var essayPopStateHandler = function (event) {
var essayContainer;
if (window.location.href.indexOf("/essays") > -1 || window.location.href.indexOf("/global") > -1 || window.location.href.indexOf("/read") > -1) {
displaySection("list", document);
} else if (event.state !== null) {
essayContainer = document.querySelector("[data-essay-author=\"" + event.state.essayAuthor + "\"][data-essay-id=\"" + event.state.essayId + "\"]");
displaySection("essay", essayContainer);
}
};
for (i = 0; i < essayContainers.length; i++) {
essayContainers[i].addEventListener("click", essayClickHandler, false);
}
window.addEventListener("popstate", essayPopStateHandler, false);
}
if (history.pushState !== undefined) {
document.addEventListener("DOMContentLoaded", init, false);
}