Forum Message UI Improvement (Thesis Board #3470) #61

Merged
tozh4728 merged 20 commits from 3470-forum-msg-ui-improvement into develop 2024-12-19 15:28:23 +01:00
12 changed files with 110 additions and 42 deletions

View File

@ -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);
} }

View File

@ -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) {

View File

@ -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);
} }

View File

@ -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();
}
} }

View File

@ -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);
} }

View File

@ -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

View File

@ -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(

View File

@ -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>

View File

@ -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) {

View File

@ -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>

View File

@ -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,14 +257,21 @@ 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() { Fragment fragment = new Fragment(id, "readStateColumnMarkupId", SupervisorMyProjectsPanel.this);
return !projectForumService.hasUnreadThreads(
long msgCount = projectForumService.getUnreadThreadsCount(
projectModel.getObject(), projectModel.getObject(),
SciProSession.get().getUser() SciProSession.get().getUser()
); );
boolean isRead = msgCount == 0;
AbstractReadStatePanel readStatePanel = new AbstractReadStatePanel("flag") {
@Override
protected boolean isRead() {
return isRead;
} }
@Override @Override
@ -267,8 +281,25 @@ public class SupervisorMyProjectsPanel extends Panel {
SupervisorThreadedForumPage.getPageParameters(projectModel.getObject()) 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);
} }
} }
} }

View File

@ -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.