Forum Message UI Improvement (Thesis Board #3470) #61
@ -1,7 +1,8 @@
|
|||||||
package se.su.dsv.scipro.forum;
|
package se.su.dsv.scipro.forum;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
|
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
|
||||||
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
|
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
|
||||||
import se.su.dsv.scipro.system.User;
|
import se.su.dsv.scipro.system.User;
|
||||||
@ -20,4 +21,6 @@ public interface BasicForumService extends Serializable {
|
|||||||
List<ForumPost> getPosts(ForumThread forumThread);
|
List<ForumPost> getPosts(ForumThread forumThread);
|
||||||
|
|
||||||
ForumThread createThread(String subject);
|
ForumThread createThread(String subject);
|
||||||
|
|
||||||
|
long countUnreadThreads(List<ForumThread> forumThreadList, User user);
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,11 @@ public class BasicForumServiceImpl implements BasicForumService {
|
|||||||
return threadRepository.save(forumThread);
|
return threadRepository.save(forumThread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long countUnreadThreads(List<ForumThread> forumThreadList, User user) {
|
||||||
|
return postRepository.countUnreadThreads(forumThreadList, user);
|
||||||
|
}
|
||||||
|
|
||||||
private ForumPostReadState getReadState(final User user, final ForumPost post) {
|
private ForumPostReadState getReadState(final User user, final ForumPost post) {
|
||||||
ForumPostReadState state = readStateRepository.find(user, post);
|
ForumPostReadState state = readStateRepository.find(user, post);
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
|
@ -8,6 +8,7 @@ import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
|
|||||||
import se.su.dsv.scipro.project.Project;
|
import se.su.dsv.scipro.project.Project;
|
||||||
import se.su.dsv.scipro.system.JpaRepository;
|
import se.su.dsv.scipro.system.JpaRepository;
|
||||||
import se.su.dsv.scipro.system.QueryDslPredicateExecutor;
|
import se.su.dsv.scipro.system.QueryDslPredicateExecutor;
|
||||||
|
import se.su.dsv.scipro.system.User;
|
||||||
import se.su.dsv.scipro.util.Pair;
|
import se.su.dsv.scipro.util.Pair;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@ -15,4 +16,6 @@ public interface ForumPostRepository extends JpaRepository<ForumPost, Long>, Que
|
|||||||
List<ForumPost> findByThread(ForumThread forumThread);
|
List<ForumPost> findByThread(ForumThread forumThread);
|
||||||
|
|
||||||
List<Pair<ProjectThread, ForumPost>> latestPost(Project project, int amount);
|
List<Pair<ProjectThread, ForumPost>> latestPost(Project project, int amount);
|
||||||
|
|
||||||
|
long countUnreadThreads(List<ForumThread> forumThreadList, User user);
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,22 @@ package se.su.dsv.scipro.forum;
|
|||||||
|
|
||||||
import static com.querydsl.core.types.dsl.Expressions.allOf;
|
import static com.querydsl.core.types.dsl.Expressions.allOf;
|
||||||
|
|
||||||
|
import com.querydsl.jpa.JPAExpressions;
|
||||||
import com.querydsl.jpa.impl.JPAQuery;
|
import com.querydsl.jpa.impl.JPAQuery;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Provider;
|
import jakarta.inject.Provider;
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
|
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
|
||||||
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
|
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
|
||||||
import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
|
import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
|
||||||
import se.su.dsv.scipro.forum.dataobjects.QForumPost;
|
import se.su.dsv.scipro.forum.dataobjects.QForumPost;
|
||||||
|
import se.su.dsv.scipro.forum.dataobjects.QForumPostReadState;
|
||||||
import se.su.dsv.scipro.forum.dataobjects.QForumThread;
|
import se.su.dsv.scipro.forum.dataobjects.QForumThread;
|
||||||
import se.su.dsv.scipro.forum.dataobjects.QProjectThread;
|
import se.su.dsv.scipro.forum.dataobjects.QProjectThread;
|
||||||
import se.su.dsv.scipro.project.Project;
|
import se.su.dsv.scipro.project.Project;
|
||||||
import se.su.dsv.scipro.system.GenericRepo;
|
import se.su.dsv.scipro.system.GenericRepo;
|
||||||
|
import se.su.dsv.scipro.system.User;
|
||||||
import se.su.dsv.scipro.util.Pair;
|
import se.su.dsv.scipro.util.Pair;
|
||||||
|
|
||||||
public class ForumPostRepositoryImpl extends GenericRepo<ForumPost, Long> implements ForumPostRepository {
|
public class ForumPostRepositoryImpl extends GenericRepo<ForumPost, Long> implements ForumPostRepository {
|
||||||
@ -44,4 +47,24 @@ public class ForumPostRepositoryImpl extends GenericRepo<ForumPost, Long> implem
|
|||||||
.map(tuple -> new Pair<>(tuple.get(QProjectThread.projectThread), tuple.get(QForumPost.forumPost)))
|
.map(tuple -> new Pair<>(tuple.get(QProjectThread.projectThread), tuple.get(QForumPost.forumPost)))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long countUnreadThreads(List<ForumThread> forumThreadList, User user) {
|
||||||
|
return new JPAQuery<>(em())
|
||||||
|
.select(QForumThread.forumThread.id.countDistinct())
|
||||||
|
.from(QForumThread.forumThread)
|
||||||
|
.leftJoin(QForumThread.forumThread.posts, QForumPost.forumPost)
|
||||||
|
.where(
|
||||||
|
QForumPost.forumPost.notIn(
|
||||||
|
JPAExpressions.select(QForumPostReadState.forumPostReadState.id.post)
|
||||||
|
.from(QForumPostReadState.forumPostReadState)
|
||||||
|
.where(
|
||||||
|
QForumPostReadState.forumPostReadState.read.isTrue(),
|
||||||
|
QForumPostReadState.forumPostReadState.id.user.eq(user)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
QForumThread.forumThread.in(forumThreadList)
|
||||||
|
)
|
||||||
|
.fetchOne();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,5 +23,5 @@ public interface ProjectForumService {
|
|||||||
// TODO: Get these away from here
|
// TODO: Get these away from here
|
||||||
List<Pair<ProjectThread, ForumPost>> latestPost(Project a, int amount);
|
List<Pair<ProjectThread, ForumPost>> latestPost(Project a, int amount);
|
||||||
|
|
||||||
boolean hasUnreadThreads(Project project, User user);
|
long getUnreadThreadsCount(Project project, User user);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ package se.su.dsv.scipro.forum;
|
|||||||
import com.google.common.eventbus.EventBus;
|
import com.google.common.eventbus.EventBus;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import se.su.dsv.scipro.file.FileSource;
|
import se.su.dsv.scipro.file.FileSource;
|
||||||
import se.su.dsv.scipro.file.ProjectFileService;
|
import se.su.dsv.scipro.file.ProjectFileService;
|
||||||
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
|
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
|
||||||
@ -114,14 +115,10 @@ public class ProjectForumServiceImpl implements ProjectForumService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasUnreadThreads(Project project, User user) {
|
public long getUnreadThreadsCount(Project project, User user) {
|
||||||
List<ProjectThread> threads = getThreads(project);
|
List<ProjectThread> threads = getThreads(project);
|
||||||
for (ProjectThread thread : threads) {
|
List<ForumThread> list = threads.stream().map(ProjectThread::getForumThread).toList();
|
||||||
if (!basicForumService.isThreadRead(user, thread.getForumThread())) {
|
return basicForumService.countUnreadThreads(list, user);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -105,9 +105,9 @@ public class ProjectForumServiceImplTest extends ForumModuleTest {
|
|||||||
final ProjectThread thread = service.createThread(project, supervisor, "subject", "content", Set.of());
|
final ProjectThread thread = service.createThread(project, supervisor, "subject", "content", Set.of());
|
||||||
service.createReply(thread, author, "reply", Set.of());
|
service.createReply(thread, author, "reply", Set.of());
|
||||||
|
|
||||||
boolean hasUnreadThreads = service.hasUnreadThreads(project, supervisor);
|
long count = service.getUnreadThreadsCount(project, supervisor);
|
||||||
|
|
||||||
assertTrue(hasUnreadThreads);
|
assertEquals(1, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertNewForumThread(
|
private void assertNewForumThread(
|
||||||
|
@ -6,9 +6,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<wicket:panel>
|
<wicket:panel>
|
||||||
<a wicket:id="toggle" href="#">
|
<a wicket:id="toggle" href="#"><span wicket:id="icon" class="fa fa-flag read-state"></span></a>
|
||||||
<span wicket:id="icon" class="fa fa-flag read-state"></span>
|
|
||||||
</a>
|
|
||||||
</wicket:panel>
|
</wicket:panel>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -10,10 +10,14 @@ import org.apache.wicket.markup.html.panel.Panel;
|
|||||||
|
|
||||||
public abstract class AbstractReadStatePanel extends Panel {
|
public abstract class AbstractReadStatePanel extends Panel {
|
||||||
|
|
||||||
private final Component icon;
|
public static final String TOGGLE = "toggle";
|
||||||
|
static final String ICON = "icon";
|
||||||
|
|
||||||
public AbstractReadStatePanel(final String id) {
|
public AbstractReadStatePanel(final String id) {
|
||||||
super(id);
|
super(id);
|
||||||
|
Component icon = new UpdatingImage(ICON);
|
||||||
|
icon.setOutputMarkupId(true);
|
||||||
|
|
||||||
AjaxFallbackLink<Void> link = new AjaxFallbackLink<>(TOGGLE) {
|
AjaxFallbackLink<Void> link = new AjaxFallbackLink<>(TOGGLE) {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(final Optional<AjaxRequestTarget> target) {
|
public void onClick(final Optional<AjaxRequestTarget> target) {
|
||||||
@ -23,20 +27,15 @@ public abstract class AbstractReadStatePanel extends Panel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
add(link);
|
|
||||||
|
|
||||||
icon = new UpdatingImage(ICON);
|
|
||||||
icon.setOutputMarkupId(true);
|
|
||||||
link.add(icon);
|
link.add(icon);
|
||||||
|
|
||||||
|
add(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract boolean isRead();
|
protected abstract boolean isRead();
|
||||||
|
|
||||||
protected abstract void onFlagClick(final AjaxRequestTarget target);
|
protected abstract void onFlagClick(final AjaxRequestTarget target);
|
||||||
|
|
||||||
public static final String TOGGLE = "toggle";
|
|
||||||
static final String ICON = "icon";
|
|
||||||
|
|
||||||
private class UpdatingImage extends WebComponent {
|
private class UpdatingImage extends WebComponent {
|
||||||
|
|
||||||
public UpdatingImage(String id) {
|
public UpdatingImage(String id) {
|
||||||
|
@ -41,6 +41,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-striped table-hover" wicket:id="dp"></table>
|
<table class="table table-striped table-hover" wicket:id="dp"></table>
|
||||||
|
|
||||||
|
<wicket:fragment wicket:id="readStateColumnMarkupId">
|
||||||
|
<span wicket:id="flag"></span>
|
||||||
|
<wicket:enclosure child="counter">
|
||||||
|
(<wicket:container wicket:id="counter"></wicket:container>)
|
||||||
|
</wicket:enclosure>
|
||||||
|
</wicket:fragment>
|
||||||
</wicket:panel>
|
</wicket:panel>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -5,6 +5,7 @@ import static java.util.Arrays.asList;
|
|||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.apache.wicket.AttributeModifier;
|
||||||
import org.apache.wicket.ajax.AjaxRequestTarget;
|
import org.apache.wicket.ajax.AjaxRequestTarget;
|
||||||
import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
|
import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
|
||||||
import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox;
|
import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox;
|
||||||
@ -14,16 +15,22 @@ import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColu
|
|||||||
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
|
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
|
||||||
import org.apache.wicket.extensions.markup.html.repeater.data.table.LambdaColumn;
|
import org.apache.wicket.extensions.markup.html.repeater.data.table.LambdaColumn;
|
||||||
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
|
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
|
||||||
|
import org.apache.wicket.markup.html.basic.Label;
|
||||||
import org.apache.wicket.markup.html.form.EnumChoiceRenderer;
|
import org.apache.wicket.markup.html.form.EnumChoiceRenderer;
|
||||||
import org.apache.wicket.markup.html.form.Form;
|
import org.apache.wicket.markup.html.form.Form;
|
||||||
import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
|
import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
|
||||||
|
import org.apache.wicket.markup.html.panel.Fragment;
|
||||||
import org.apache.wicket.markup.html.panel.Panel;
|
import org.apache.wicket.markup.html.panel.Panel;
|
||||||
import org.apache.wicket.markup.repeater.Item;
|
import org.apache.wicket.markup.repeater.Item;
|
||||||
import org.apache.wicket.model.IModel;
|
import org.apache.wicket.model.IModel;
|
||||||
import org.apache.wicket.model.LambdaModel;
|
import org.apache.wicket.model.LambdaModel;
|
||||||
import org.apache.wicket.model.LoadableDetachableModel;
|
import org.apache.wicket.model.LoadableDetachableModel;
|
||||||
import org.apache.wicket.model.Model;
|
import org.apache.wicket.model.Model;
|
||||||
import se.su.dsv.scipro.components.*;
|
import se.su.dsv.scipro.components.AjaxCheckBoxMultipleChoice;
|
||||||
|
import se.su.dsv.scipro.components.BootstrapRadioChoice;
|
||||||
|
import se.su.dsv.scipro.components.ExportableDataPanel;
|
||||||
|
import se.su.dsv.scipro.components.ListAdapterModel;
|
||||||
|
import se.su.dsv.scipro.components.TemporalColumn;
|
||||||
import se.su.dsv.scipro.components.datatables.MultipleUsersColumn;
|
import se.su.dsv.scipro.components.datatables.MultipleUsersColumn;
|
||||||
import se.su.dsv.scipro.components.datatables.UserColumn;
|
import se.su.dsv.scipro.components.datatables.UserColumn;
|
||||||
import se.su.dsv.scipro.dataproviders.FilteredDataProvider;
|
import se.su.dsv.scipro.dataproviders.FilteredDataProvider;
|
||||||
@ -250,25 +257,49 @@ public class SupervisorMyProjectsPanel extends Panel {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void populateItem(Item<ICellPopulator<Project>> item, String id, IModel<Project> projectModel) {
|
public void populateItem(Item<ICellPopulator<Project>> item, String id, IModel<Project> projectModel) {
|
||||||
item.add(
|
// Since table cell only can contain one item, we use Wicket Fragment here. It will contain two components,
|
||||||
new AbstractReadStatePanel(id) {
|
// one for flag, one for unread messages counter.
|
||||||
@Override
|
|
||||||
protected boolean isRead() {
|
|
||||||
return !projectForumService.hasUnreadThreads(
|
|
||||||
projectModel.getObject(),
|
|
||||||
SciProSession.get().getUser()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
Fragment fragment = new Fragment(id, "readStateColumnMarkupId", SupervisorMyProjectsPanel.this);
|
||||||
protected void onFlagClick(AjaxRequestTarget target) {
|
|
||||||
setResponsePage(
|
long msgCount = projectForumService.getUnreadThreadsCount(
|
||||||
SupervisorThreadedForumPage.class,
|
projectModel.getObject(),
|
||||||
SupervisorThreadedForumPage.getPageParameters(projectModel.getObject())
|
SciProSession.get().getUser()
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
boolean isRead = msgCount == 0;
|
||||||
|
|
||||||
|
AbstractReadStatePanel readStatePanel = new AbstractReadStatePanel("flag") {
|
||||||
|
@Override
|
||||||
|
protected boolean isRead() {
|
||||||
|
return isRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onFlagClick(AjaxRequestTarget target) {
|
||||||
|
setResponsePage(
|
||||||
|
SupervisorThreadedForumPage.class,
|
||||||
|
SupervisorThreadedForumPage.getPageParameters(projectModel.getObject())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isRead) {
|
||||||
|
readStatePanel.add(new AttributeModifier("title", getString("unread.msg")));
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment.add(readStatePanel);
|
||||||
|
|
||||||
|
Label counterLabel = new Label("counter", msgCount) {
|
||||||
|
@Override
|
||||||
|
protected void onConfigure() {
|
||||||
|
super.onConfigure();
|
||||||
|
setVisible(msgCount > 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fragment.add(counterLabel);
|
||||||
|
|
||||||
|
item.add(fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,3 +12,5 @@ ProjectStatus.COMPLETED= Completed
|
|||||||
|
|
||||||
SupervisorProjectNoteDisplay.COMPACT=Compact
|
SupervisorProjectNoteDisplay.COMPACT=Compact
|
||||||
SupervisorProjectNoteDisplay.FULL=Full
|
SupervisorProjectNoteDisplay.FULL=Full
|
||||||
|
|
||||||
|
unread.msg=There are unread messages.
|
Loading…
x
Reference in New Issue
Block a user