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;
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.ForumThread;
import se.su.dsv.scipro.system.User;
@ -20,4 +21,6 @@ public interface BasicForumService extends Serializable {
List<ForumPost> getPosts(ForumThread forumThread);
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);
}
@Override
public long countUnreadThreads(List<ForumThread> forumThreadList, User user) {
return postRepository.countUnreadThreads(forumThreadList, user);
}
private ForumPostReadState getReadState(final User user, final ForumPost post) {
ForumPostReadState state = readStateRepository.find(user, post);
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.system.JpaRepository;
import se.su.dsv.scipro.system.QueryDslPredicateExecutor;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Pair;
@Transactional
@ -15,4 +16,6 @@ public interface ForumPostRepository extends JpaRepository<ForumPost, Long>, Que
List<ForumPost> findByThread(ForumThread forumThread);
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 com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQuery;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
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.ForumThread;
import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
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.QProjectThread;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.GenericRepo;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Pair;
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)))
.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
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 jakarta.inject.Inject;
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.ProjectFileService;
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
@ -114,14 +115,10 @@ public class ProjectForumServiceImpl implements ProjectForumService {
}
@Override
public boolean hasUnreadThreads(Project project, User user) {
public long getUnreadThreadsCount(Project project, User user) {
List<ProjectThread> threads = getThreads(project);
for (ProjectThread thread : threads) {
if (!basicForumService.isThreadRead(user, thread.getForumThread())) {
return true;
}
}
return false;
List<ForumThread> list = threads.stream().map(ProjectThread::getForumThread).toList();
return basicForumService.countUnreadThreads(list, user);
}
@Override

View File

@ -105,9 +105,9 @@ public class ProjectForumServiceImplTest extends ForumModuleTest {
final ProjectThread thread = service.createThread(project, supervisor, "subject", "content", 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(

View File

@ -6,9 +6,7 @@
</head>
<body>
<wicket:panel>
<a wicket:id="toggle" href="#">
<span wicket:id="icon" class="fa fa-flag read-state"></span>
</a>
<a wicket:id="toggle" href="#"><span wicket:id="icon" class="fa fa-flag read-state"></span></a>
</wicket:panel>
</body>
</html>

View File

@ -10,10 +10,14 @@ import org.apache.wicket.markup.html.panel.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) {
super(id);
Component icon = new UpdatingImage(ICON);
icon.setOutputMarkupId(true);
AjaxFallbackLink<Void> link = new AjaxFallbackLink<>(TOGGLE) {
@Override
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);
add(link);
}
protected abstract boolean isRead();
protected abstract void onFlagClick(final AjaxRequestTarget target);
public static final String TOGGLE = "toggle";
static final String ICON = "icon";
private class UpdatingImage extends WebComponent {
public UpdatingImage(String id) {

View File

@ -41,6 +41,13 @@
</div>
<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>
</body>
</html>

View File

@ -5,6 +5,7 @@ import static java.util.Arrays.asList;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
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.LambdaColumn;
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.Form;
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.repeater.Item;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel;
import org.apache.wicket.model.LoadableDetachableModel;
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.UserColumn;
import se.su.dsv.scipro.dataproviders.FilteredDataProvider;
@ -250,25 +257,49 @@ public class SupervisorMyProjectsPanel extends Panel {
@Override
public void populateItem(Item<ICellPopulator<Project>> item, String id, IModel<Project> projectModel) {
item.add(
new AbstractReadStatePanel(id) {
@Override
protected boolean isRead() {
return !projectForumService.hasUnreadThreads(
projectModel.getObject(),
SciProSession.get().getUser()
);
}
// Since table cell only can contain one item, we use Wicket Fragment here. It will contain two components,
// one for flag, one for unread messages counter.
@Override
protected void onFlagClick(AjaxRequestTarget target) {
setResponsePage(
SupervisorThreadedForumPage.class,
SupervisorThreadedForumPage.getPageParameters(projectModel.getObject())
);
}
}
Fragment fragment = new Fragment(id, "readStateColumnMarkupId", SupervisorMyProjectsPanel.this);
long msgCount = projectForumService.getUnreadThreadsCount(
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);
}
}
}

View File

@ -12,3 +12,5 @@ ProjectStatus.COMPLETED= Completed
SupervisorProjectNoteDisplay.COMPACT=Compact
SupervisorProjectNoteDisplay.FULL=Full
unread.msg=There are unread messages.