Using pushState to navigate from essay list to single essay
This commit is contained in:
parent
83ed302667
commit
ac4f1331b5
11 changed files with 176 additions and 78 deletions
16
src/main/java/com/moandjiezana/essayist/utils/Tasks.java
Normal file
16
src/main/java/com/moandjiezana/essayist/utils/Tasks.java
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</&>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
65
src/main/webapp/assets/essayist.js
Normal file
65
src/main/webapp/assets/essayist.js
Normal 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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue