From 959498532fdca4e875000325f3d0c4f6b30f3915 Mon Sep 17 00:00:00 2001
From: uhensler <urs.hensler@frentix.com>
Date: Mon, 5 Aug 2019 09:13:17 +0200
Subject: [PATCH] OO-4091: Mark and "new" columns in forum messages list

---
 .../commons/services/mark/impl/MarkImpl.java  |   3 +
 .../services/mark/impl/ui/MarkController.java |   2 +
 .../services/mark/impl/ui/MarkedEvent.java    |  38 +++++++
 .../services/mark/impl/ui/UnmarkedEvent.java  |  38 +++++++
 .../course/run/preview/PreviewIdentity.java   |   2 +-
 .../fo/_i18n/LocalStrings_de.properties       |  15 ++-
 .../fo/_i18n/LocalStrings_en.properties       |   3 +
 .../modules/fo/ui/ForumMessageDataModel.java  |  36 +++---
 .../fo/ui/ForumMessageListController.java     | 106 ++++++++++++++++--
 .../olat/modules/fo/ui/MessageLightView.java  |  12 +-
 .../modules/fo/ui/MessageLightViewRow.java    |  62 ++++++++++
 .../modules/fo/ui/MessageListController.java  |  28 +++--
 .../modules/fo/ui/MessageMarkedEvent.java     |  51 +++++++++
 .../org/olat/modules/fo/ui/MessageView.java   |  20 +---
 14 files changed, 359 insertions(+), 57 deletions(-)
 create mode 100644 src/main/java/org/olat/core/commons/services/mark/impl/ui/MarkedEvent.java
 create mode 100644 src/main/java/org/olat/core/commons/services/mark/impl/ui/UnmarkedEvent.java
 create mode 100644 src/main/java/org/olat/modules/fo/ui/MessageLightViewRow.java
 create mode 100644 src/main/java/org/olat/modules/fo/ui/MessageMarkedEvent.java

diff --git a/src/main/java/org/olat/core/commons/services/mark/impl/MarkImpl.java b/src/main/java/org/olat/core/commons/services/mark/impl/MarkImpl.java
index bc3081db0dd..b4725c01d61 100644
--- a/src/main/java/org/olat/core/commons/services/mark/impl/MarkImpl.java
+++ b/src/main/java/org/olat/core/commons/services/mark/impl/MarkImpl.java
@@ -78,6 +78,7 @@ public class MarkImpl extends PersistentObject implements Mark {
 		this.resId = resId;
 	}
 
+	@Override
 	public String getResSubPath() {
 		return resSubPath;
 	}
@@ -86,6 +87,7 @@ public class MarkImpl extends PersistentObject implements Mark {
 		this.resSubPath = resSubPath;
 	}
 
+	@Override
 	public String getBusinessPath() {
 		return businessPath;
 	}
@@ -94,6 +96,7 @@ public class MarkImpl extends PersistentObject implements Mark {
 		this.businessPath = businessPath;
 	}
 
+	@Override
 	public Identity getCreator() {
 		return creator;
 	}
diff --git a/src/main/java/org/olat/core/commons/services/mark/impl/ui/MarkController.java b/src/main/java/org/olat/core/commons/services/mark/impl/ui/MarkController.java
index 6be1096c3da..5214b068817 100644
--- a/src/main/java/org/olat/core/commons/services/mark/impl/ui/MarkController.java
+++ b/src/main/java/org/olat/core/commons/services/mark/impl/ui/MarkController.java
@@ -145,8 +145,10 @@ public class MarkController extends FormBasicController {
 					markingService.getMarkManager().removeMark(mark);
 					mark = null;
 				}
+				fireEvent(ureq, new UnmarkedEvent());
 			} else {
 				mark = markingService.getMarkManager().setMark(ores, identity, subPath, businessPath);
+				fireEvent(ureq, new MarkedEvent());
 			}
 			marked = !marked;
 			markLink.setIconLeftCSS(marked ? Mark.MARK_CSS_LARGE : Mark.MARK_ADD_CSS_LARGE);
diff --git a/src/main/java/org/olat/core/commons/services/mark/impl/ui/MarkedEvent.java b/src/main/java/org/olat/core/commons/services/mark/impl/ui/MarkedEvent.java
new file mode 100644
index 00000000000..805d02505ed
--- /dev/null
+++ b/src/main/java/org/olat/core/commons/services/mark/impl/ui/MarkedEvent.java
@@ -0,0 +1,38 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.commons.services.mark.impl.ui;
+
+import org.olat.core.gui.control.Event;
+
+/**
+ * 
+ * Initial date: 5 Aug 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class MarkedEvent extends Event {
+
+	private static final long serialVersionUID = -4104070726631981649L;
+
+	public MarkedEvent() {
+		super("marked");
+	}
+
+}
diff --git a/src/main/java/org/olat/core/commons/services/mark/impl/ui/UnmarkedEvent.java b/src/main/java/org/olat/core/commons/services/mark/impl/ui/UnmarkedEvent.java
new file mode 100644
index 00000000000..3fc40176532
--- /dev/null
+++ b/src/main/java/org/olat/core/commons/services/mark/impl/ui/UnmarkedEvent.java
@@ -0,0 +1,38 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.commons.services.mark.impl.ui;
+
+import org.olat.core.gui.control.Event;
+
+/**
+ * 
+ * Initial date: 5 Aug 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class UnmarkedEvent extends Event {
+
+	private static final long serialVersionUID = 3503240359856342650L;
+
+	public UnmarkedEvent() {
+		super("unmarked");
+	}
+
+}
diff --git a/src/main/java/org/olat/course/run/preview/PreviewIdentity.java b/src/main/java/org/olat/course/run/preview/PreviewIdentity.java
index 5bfdbd1b9df..071a1a43250 100644
--- a/src/main/java/org/olat/course/run/preview/PreviewIdentity.java
+++ b/src/main/java/org/olat/course/run/preview/PreviewIdentity.java
@@ -60,7 +60,7 @@ public final class PreviewIdentity implements Identity, User {
 	}
 
 	/**
-	 * @see org.olat.core.commons.persistence.Persistable#getKey()
+	 * @see org.olat.core.commons.persistence.Persistable#getSelectedMessageKey()
 	 */
 	@Override
 	public Long getKey() {
diff --git a/src/main/java/org/olat/modules/fo/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/fo/_i18n/LocalStrings_de.properties
index 2d634895c9d..e8c50bbff71 100644
--- a/src/main/java/org/olat/modules/fo/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/modules/fo/_i18n/LocalStrings_de.properties
@@ -22,12 +22,12 @@ archive.thread.dialog=Wollen Sie dieses Diskussionsthema wirklich in Ihrem pers\
 archive.thread.successfully=Das Diskussionsthema wurde in Ihrem pers\u00F6nlichen Ordner im Unterordner "private/archive" archiviert.
 attachments=Dateianh\u00E4nge
 attachments.too.big=Dateianhang ist zu gross. Es sind maximal {0} MB m\u00F6glich.
-attachments.upload.successful=Die Datei {0} wurde erfolgreich hochgeladen. Bei Bedarf k\u00f6nnen noch weitere Dateien angeh\u00e4ngt werden.
+attachments.upload.successful=Die Datei {0} wurde erfolgreich hochgeladen. Bei Bedarf k\u00F6nnen noch weitere Dateien angeh\u00E4ngt werden.
 attachments.error.file.exists=Diese Datei existiert bereits und kann nicht erneut hochgeladen werden.
-attachments.remove.string=l\u00f6schen
+attachments.remove.string=l\u00F6schen
 close.thread=Diskussion beenden
-confirm.delete.pseudonym.title=Pseudonym l\u00f6schen
-confirm.detele.pseudonym.msg=Wollen Sie wirklich den Pseudonym "{0}" mit {1} Forum Beitr\u00E4ge l\u00f6schen?
+confirm.delete.pseudonym.title=Pseudonym l\u00F6schen
+confirm.detele.pseudonym.msg=Wollen Sie wirklich den Pseudonym "{0}" mit {1} Forum Beitr\u00E4ge l\u00F6schen?
 create.pseudonym=Pseudonym erstellen
 delete.att.ok=Die Datei wurde gel\u00F6scht.
 deleteok=Der Beitrag wurde gel\u00F6scht
@@ -110,14 +110,17 @@ remove.sticky= Entfernt Priorisierung (sticky)
 show.thread=Diskussion anzeigen
 table.closed=Beendet
 table.download=Tabelle downloaden
-table.entries=Eintr\u00e4ge
+table.entries=Eintr\u00E4ge
 table.entry=Eintrag
+table.header.mark=<i class\="o_icon o_icon_bookmark_header o_icon-lg" title\="Favorit"> </i>
+table.header.new.message=<i class\="o_icon o_forum_new_icon o_icon-lg" title\="$\:table.new.message.hover"> </i>
 table.header.state=Zustand
 table.header.typeimg=Typ
 table.hidden=Unsichtbar
 table.lastModified=Letzte \u00C4nderung
 table.marked=Markiert
 table.modified=\u00C4nderungsdatum
+table.new.message.hover=Neuer Beitrag
 table.numOfCharacters=Zeichen
 table.numOfWords=W\u00F6rter
 table.row.new=Neu
@@ -140,7 +143,7 @@ viewswitch.marked=markiert
 viewswitch.new=neu
 viewswitch.title=Beitr\u00E4ge anzeigen
 yes=Ja
-may.not.move.message = Sie haben nicht gen\u00fcgend Berechtigungen, um diesen Beitrag zu verschieben.
+may.not.move.message = Sie haben nicht gen\u00FCgend Berechtigungen, um diesen Beitrag zu verschieben.
 msg.move = Verschieben in anderes Thema
 msg.exile=Verschieben in anderes Forum
 msg.moved = Dieser Beitrag wurde aus einem anderen Thema hierhin verschoben.
diff --git a/src/main/java/org/olat/modules/fo/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/fo/_i18n/LocalStrings_en.properties
index ebd9f691d7e..7cd154ccd0b 100644
--- a/src/main/java/org/olat/modules/fo/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/modules/fo/_i18n/LocalStrings_en.properties
@@ -117,12 +117,15 @@ table.closed=Completed
 table.download=Download table
 table.entries=Entries
 table.entry=Entry
+table.header.mark=<i class\="o_icon o_icon_bookmark_header o_icon-lg" title\="Favorit"> </i>
+table.header.new.message=<i class\="o_icon o_forum_new_icon" title\="$\:table.new.message.hover"> </i>
 table.header.state=Status
 table.header.typeimg=Type
 table.hidden=Invisible
 table.lastModified=Last modified
 table.marked=Marked
 table.modified=Modified at
+table.new.message.hover=New post
 table.numOfCharacters=Characters
 table.numOfWords=Words
 table.row.new=New
diff --git a/src/main/java/org/olat/modules/fo/ui/ForumMessageDataModel.java b/src/main/java/org/olat/modules/fo/ui/ForumMessageDataModel.java
index 5cac682540a..eab2a128e4e 100644
--- a/src/main/java/org/olat/modules/fo/ui/ForumMessageDataModel.java
+++ b/src/main/java/org/olat/modules/fo/ui/ForumMessageDataModel.java
@@ -38,8 +38,8 @@ import org.olat.group.ui.main.AbstractMemberListController;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class ForumMessageDataModel extends DefaultFlexiTableDataModel<MessageLightView>
-	implements SortableFlexiTableDataModel<MessageLightView> {
+public class ForumMessageDataModel extends DefaultFlexiTableDataModel<MessageLightViewRow>
+	implements SortableFlexiTableDataModel<MessageLightViewRow> {
 	
 	private Translator translator;
 	
@@ -54,7 +54,7 @@ public class ForumMessageDataModel extends DefaultFlexiTableDataModel<MessageLig
 			if("natural".equals(orderBy.getKey())) {
 				//System.out.println();
 			} else {
-				List<MessageLightView> views = new ForumMessageDataModelSort(orderBy, this, null).sort();
+				List<MessageLightViewRow> views = new ForumMessageDataModelSort(orderBy, this, null).sort();
 				super.setObjects(views);
 			}
 		}
@@ -62,40 +62,44 @@ public class ForumMessageDataModel extends DefaultFlexiTableDataModel<MessageLig
 	
 	@Override
 	public Object getValueAt(int row, int col) {
-		MessageLightView view = getObject(row);
+		MessageLightViewRow view = getObject(row);
 		return getValueAt(view, col);
 	}
 
 	@Override
-	public Object getValueAt(MessageLightView row, int col) {
+	public Object getValueAt(MessageLightViewRow row, int col) {
 		if(col >= 0 && col < ForumMessageCols.values().length) {
 			switch(ForumMessageCols.values()[col]) {
-				case type: return row.getStatusCode();
-				case thread: return StringHelper.escapeHtml(row.getTitle());
-				case lastModified: return row.getLastModified();
+				case type: return row.getView().getStatusCode();
+				case mark: return row.getMarkLink();
+				case thread: return StringHelper.escapeHtml(row.getView().getTitle());
+				case lastModified: return row.getView().getLastModified();
+				case newMessage: return row.getView().isNewMessage()? Boolean.TRUE: null;
 				default: return "ERROR";
 			}
 		}
 		
 		int propPos = col - AbstractMemberListController.USER_PROPS_OFFSET;
-		if(StringHelper.containsNonWhitespace(row.getPseudonym())) {
-			return propPos == 0 ? row.getPseudonym() : null;
+		if(StringHelper.containsNonWhitespace(row.getView().getPseudonym())) {
+			return propPos == 0 ? row.getView().getPseudonym() : null;
 		}
-		if(row.isGuest()) {
+		if(row.getView().isGuest()) {
 			return propPos == 0 ? translator.translate("guest") : null;
 		}
-		return row.getIdentityProp(propPos);
+		return row.getView().getIdentityProp(propPos);
 	}
 
 	@Override
-	public DefaultFlexiTableDataModel<MessageLightView> createCopyWithEmptyList() {
+	public DefaultFlexiTableDataModel<MessageLightViewRow> createCopyWithEmptyList() {
 		return new ForumMessageDataModel(getTableColumnModel(), translator);
 	}
 	
 	public enum ForumMessageCols implements FlexiSortableColumnDef {
 		type("table.header.typeimg"),
+		mark("table.header.mark"),
 		thread("table.thread"),
-		lastModified("table.lastModified");
+		lastModified("table.lastModified"),
+		newMessage("table.header.new.message");
 		
 		private final String i18nKey;
 		
@@ -119,9 +123,9 @@ public class ForumMessageDataModel extends DefaultFlexiTableDataModel<MessageLig
 		}
 	}
 	
-	public class ForumMessageDataModelSort extends SortableFlexiTableModelDelegate<MessageLightView> {
+	public class ForumMessageDataModelSort extends SortableFlexiTableModelDelegate<MessageLightViewRow> {
 		
-		public ForumMessageDataModelSort(SortKey orderBy, SortableFlexiTableDataModel<MessageLightView> tableModel, Locale locale) {
+		public ForumMessageDataModelSort(SortKey orderBy, SortableFlexiTableDataModel<MessageLightViewRow> tableModel, Locale locale) {
 			super(orderBy, tableModel, locale);
 		}
 	}
diff --git a/src/main/java/org/olat/modules/fo/ui/ForumMessageListController.java b/src/main/java/org/olat/modules/fo/ui/ForumMessageListController.java
index 1d86d5168a5..d3851ac74e3 100644
--- a/src/main/java/org/olat/modules/fo/ui/ForumMessageListController.java
+++ b/src/main/java/org/olat/modules/fo/ui/ForumMessageListController.java
@@ -27,18 +27,23 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.olat.basesecurity.BaseSecurityModule;
+import org.olat.core.commons.services.mark.Mark;
+import org.olat.core.commons.services.mark.MarkingService;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.form.flexible.FormItem;
 import org.olat.core.gui.components.form.flexible.FormItemContainer;
 import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement;
 import org.olat.core.gui.components.form.flexible.elements.FlexiTableSort;
 import org.olat.core.gui.components.form.flexible.elements.FlexiTableSortOptions;
+import org.olat.core.gui.components.form.flexible.elements.FormLink;
 import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
 import org.olat.core.gui.components.form.flexible.impl.FormEvent;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiColumnModel;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableCssDelegate;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiCellRenderer;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiColumnModel;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelFactory;
@@ -46,10 +51,14 @@ import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTable
 import org.olat.core.gui.components.form.flexible.impl.elements.table.SelectionEvent;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.StaticFlexiCellRenderer;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.TextFlexiCellRenderer;
+import org.olat.core.gui.components.link.Link;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.WindowControl;
+import org.olat.core.id.OLATResourceable;
 import org.olat.core.id.UserConstants;
 import org.olat.core.util.Util;
+import org.olat.core.util.mail.ui.BooleanCSSCellRenderer;
+import org.olat.core.util.resource.OresHelper;
 import org.olat.modules.fo.Forum;
 import org.olat.modules.fo.MessageLight;
 import org.olat.modules.fo.Status;
@@ -77,7 +86,11 @@ public class ForumMessageListController extends FormBasicController {
 	
 	private final Forum forum;
 	private final boolean withType;
+	private final boolean showMarks;
+	private final boolean showNew;
+	private final boolean guestOnly;
 	private final boolean isAdministrativeUser;
+	private final OLATResourceable forumOres;
 	private final List<UserPropertyHandler> userPropertyHandlers;
 	private MessageView userObject, selectView;
 	
@@ -86,16 +99,22 @@ public class ForumMessageListController extends FormBasicController {
 	@Autowired
 	private ForumManager forumManager;
 	@Autowired
+	private MarkingService markingService;
+	@Autowired
 	private BaseSecurityModule securityModule;
 	
 	public ForumMessageListController(UserRequest ureq, WindowControl wControl,
-			Forum forum, boolean withType) {
+			Forum forum, boolean withType, boolean showMarks, boolean showNew) {
 		super(ureq, wControl, LAYOUT_BAREBONE);
 		setTranslator(Util.createPackageTranslator(Forum.class, getLocale(), getTranslator()));
 		setTranslator(userManager.getPropertyHandlerTranslator(getTranslator()));
 		
 		this.forum = forum;
 		this.withType = withType;
+		this.showMarks = showMarks;
+		this.showNew = showNew;
+		this.guestOnly = ureq.getUserSession().getRoles().isGuestOnly();
+		forumOres = OresHelper.createOLATResourceableInstance("Forum", forum.getKey());
 		
 		isAdministrativeUser = securityModule.isUserAllowedAdminProps(ureq.getUserSession().getRoles());
 		userPropertyHandlers = userManager.getUserPropertyHandlersFor(USER_PROPS_ID, isAdministrativeUser);
@@ -120,15 +139,21 @@ public class ForumMessageListController extends FormBasicController {
 	}
 
 	public void loadAllMessages() {
+		Set<Long> readSet = !guestOnly? forumManager.getReadSet(getIdentity(), forum): Collections.emptySet();
 		List<MessageLight> allMessages = forumManager.getLightMessagesByForum(forum);
 		List<MessageLightView> views = new ArrayList<>(allMessages.size());
 		Map<Long,MessageLightView> keyToViews = new HashMap<>();
 		for(MessageLight message:allMessages) {
 			MessageLightView view = new MessageLightView(message, userPropertyHandlers, getLocale());
+			if (readSet.contains(message.getKey())) {
+				view.setNewMessage(false);
+			} else {
+				view.setNewMessage(true);
+			}
 			views.add(view);
 			keyToViews.put(view.getKey(), view);
 		}
-
+		
 		//calculate depth
 		Map<Long, List<Long>> keyToParentline = new HashMap<>();
 		for(MessageLightView view:views) {
@@ -150,15 +175,40 @@ public class ForumMessageListController extends FormBasicController {
 		Collections.sort(threads, new MessageNodeComparator());
 		List<MessageLightView> orderedViews = new ArrayList<>(allMessages.size());
 		flatTree(threads, orderedViews);
-		dataModel.setObjects(orderedViews);
+		List<MessageLightViewRow> rows = appendLinks(orderedViews);
+		dataModel.setObjects(rows);
 	}
 	
 	public void loadMessages(List<MessageLightView> views) {
-		dataModel.setObjects(views);
+		List<MessageLightViewRow> rows = appendLinks(views);
+		dataModel.setObjects(rows);
 		tableEl.reloadData();
 		tableEl.reset();
 	}
 
+	private List<MessageLightViewRow> appendLinks(List<MessageLightView> views) {
+		List<Mark> markList = !guestOnly
+				? markingService.getMarkManager().getMarks(forumOres, getIdentity(), null)
+				: Collections.emptyList();
+		Map<String,Mark> marks = new HashMap<>();
+		for (Mark mark : markList) {
+			marks.put(mark.getResSubPath(), mark);
+		}
+		
+		List<MessageLightViewRow> rows = new ArrayList<>(views.size());
+		for (MessageLightView view : views) {
+			Mark mark = marks.get(view.getKey().toString());
+			boolean marked = mark != null;
+			FormLink markLink = uifactory.addFormLink("mark_" + view.getKey(), "mark", "", null, null, Link.NONTRANSLATED);
+			markLink.setIconLeftCSS(marked? Mark.MARK_CSS_LARGE : Mark.MARK_ADD_CSS_LARGE);
+			
+			MessageLightViewRow row = new MessageLightViewRow(view, mark, markLink);
+			markLink.setUserObject(row);
+			rows.add(row);
+		}
+		return rows;
+	}
+
 	@Override
 	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
 		List<FlexiTableSort> sorts = new ArrayList<>();
@@ -169,6 +219,11 @@ public class ForumMessageListController extends FormBasicController {
 			columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ForumMessageCols.type, new StatusTypeCellRenderer()));
 			sorts.add(new FlexiTableSort(translate(ForumMessageCols.type.i18nHeaderKey()), ForumMessageCols.type.name()));
 		}
+		
+		if (showMarks) {
+			columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ForumMessageCols.mark));
+		}
+		
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ForumMessageCols.thread,
 				"select", new StaticFlexiCellRenderer("select", new IndentCellRenderer())));
 		sorts.add(new FlexiTableSort(translate(ForumMessageCols.thread.i18nHeaderKey()), ForumMessageCols.thread.name()));
@@ -196,6 +251,12 @@ public class ForumMessageListController extends FormBasicController {
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ForumMessageCols.lastModified));
 		sorts.add(new FlexiTableSort(translate(ForumMessageCols.lastModified.i18nHeaderKey()), ForumMessageCols.lastModified.name()));
 
+		if(showNew && !guestOnly) {
+			FlexiCellRenderer newMessageRenderer = new BooleanCSSCellRenderer(getTranslator(),
+					"o_icon o_forum_new_icon", null, "table.new.message.hover", null);
+			columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ForumMessageCols.newMessage, newMessageRenderer));
+		}
+
 		dataModel = new ForumMessageDataModel(columnsModel, getTranslator());
 		tableEl = uifactory.addTableElement(getWindowControl(), "messages", dataModel, getTranslator(), formLayout);
 		tableEl.setCssDelegate(new MessageCssDelegate());
@@ -223,14 +284,45 @@ public class ForumMessageListController extends FormBasicController {
 				SelectionEvent se = (SelectionEvent)event;
 				String cmd = se.getCommand();
 				if("select".equals(cmd)) {
-					MessageLightView message = dataModel.getObject(se.getIndex());
+					MessageLightView message = dataModel.getObject(se.getIndex()).getView();
 					fireEvent(ureq, new SelectMessageEvent(SelectMessageEvent.SELECT_MESSAGE, message.getKey()));
 				}
 			}
+		} else if(source instanceof FormLink) {
+			FormLink link = (FormLink)source;
+			String cmd = link.getCmd();
+			if ("mark".equals(cmd)) {
+				MessageLightViewRow row = (MessageLightViewRow) link.getUserObject();
+				if (row.isMarked()) {
+					doUnmark(row);
+					link.setIconLeftCSS(Mark.MARK_ADD_CSS_LARGE);
+				} else {
+					doMark(row);
+					link.setIconLeftCSS(Mark.MARK_CSS_LARGE);
+				}
+				link.getComponent().setDirty(true);
+				fireEvent(ureq, new MessageMarkedEvent(selectView.getKey(), row.getView().getKey()));
+			}
 		}
 		super.formInnerEvent(ureq, source, event);
 	}
-	
+
+	private void doMark(MessageLightViewRow row) {
+		MessageLightView view = row.getView();
+		Mark currentMark = row.getMark();
+		String businessPath = currentMark == null ?
+				getWindowControl().getBusinessControl().getAsString() + "[Message:" + view.getKey() + "]"
+				: currentMark.getBusinessPath();
+		Mark mark = markingService.getMarkManager().setMark(forumOres, getIdentity(), view.getKey().toString(), businessPath);
+		row.setMark(mark);
+	}
+
+	private void doUnmark(MessageLightViewRow row) {
+		MessageLightView view = row.getView();
+		markingService.getMarkManager().removeMark(forumOres, getIdentity(), view.getKey().toString());
+		row.setMark(null);
+	}
+
 	private void flatTree(List<MessageNode> nodes, List<MessageLightView> orderedViews) {
 		for(MessageNode node:nodes) {
 			orderedViews.add(node.getView());
@@ -269,7 +361,7 @@ public class ForumMessageListController extends FormBasicController {
 	private class MessageCssDelegate extends DefaultFlexiTableCssDelegate {
 		@Override
 		public String getRowCssClass(FlexiTableRendererType type, int pos) {
-			MessageLightView row = dataModel.getObject(pos);
+			MessageLightView row = dataModel.getObject(pos).getView();
 			return row != null && selectView != null && row.getKey().equals(selectView.getKey()) ? "o_row_selected" : null;
 		}
 	}
diff --git a/src/main/java/org/olat/modules/fo/ui/MessageLightView.java b/src/main/java/org/olat/modules/fo/ui/MessageLightView.java
index ba4d21a3714..016f7495ebf 100644
--- a/src/main/java/org/olat/modules/fo/ui/MessageLightView.java
+++ b/src/main/java/org/olat/modules/fo/ui/MessageLightView.java
@@ -48,9 +48,11 @@ public class MessageLightView extends UserPropertiesRow implements MessageRef {
 
 	private final Long threadtopKey;
 
-	private int depth;
+	private int depth = -1;
 	private int numOfChildren = 0;
 
+	private boolean newMessage;
+
 	public MessageLightView(MessageLight message, List<UserPropertyHandler> userPropertyHandlers, Locale locale) {
 		super(message.getCreator(), userPropertyHandlers, locale);
 		key = message.getKey();
@@ -120,4 +122,12 @@ public class MessageLightView extends UserPropertiesRow implements MessageRef {
 	public boolean isThreadTop() {
 		return threadtopKey != null;
 	}
+
+	public boolean isNewMessage() {
+		return newMessage;
+	}
+
+	public void setNewMessage(boolean newMessage) {
+		this.newMessage = newMessage;
+	}
 }
diff --git a/src/main/java/org/olat/modules/fo/ui/MessageLightViewRow.java b/src/main/java/org/olat/modules/fo/ui/MessageLightViewRow.java
new file mode 100644
index 00000000000..beaa850b8cc
--- /dev/null
+++ b/src/main/java/org/olat/modules/fo/ui/MessageLightViewRow.java
@@ -0,0 +1,62 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.fo.ui;
+
+import org.olat.core.commons.services.mark.Mark;
+import org.olat.core.gui.components.form.flexible.elements.FormLink;
+
+/**
+ * 
+ * Initial date: 31 Jul 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class MessageLightViewRow {
+
+	private final MessageLightView view;
+	private Mark mark;
+	private final FormLink markLink;
+	
+	public MessageLightViewRow(MessageLightView view, Mark mark, FormLink markLink) {
+		this.view = view;
+		this.mark = mark;
+		this.markLink = markLink;
+	}
+	
+	public MessageLightView getView() {
+		return view;
+	}
+
+	public void setMark(Mark mark) {
+		this.mark = mark;
+	}
+
+	public Mark getMark() {
+		return mark;
+	}
+
+	public boolean isMarked() {
+		return mark != null;
+	}
+
+	public FormLink getMarkLink() {
+		return markLink;
+	}
+}
diff --git a/src/main/java/org/olat/modules/fo/ui/MessageListController.java b/src/main/java/org/olat/modules/fo/ui/MessageListController.java
index 453cb2fd2ad..f5cb5726be3 100644
--- a/src/main/java/org/olat/modules/fo/ui/MessageListController.java
+++ b/src/main/java/org/olat/modules/fo/ui/MessageListController.java
@@ -35,6 +35,8 @@ import org.olat.core.commons.persistence.DBFactory;
 import org.olat.core.commons.services.mark.Mark;
 import org.olat.core.commons.services.mark.MarkResourceStat;
 import org.olat.core.commons.services.mark.MarkingService;
+import org.olat.core.commons.services.mark.impl.ui.MarkedEvent;
+import org.olat.core.commons.services.mark.impl.ui.UnmarkedEvent;
 import org.olat.core.commons.services.vfs.VFSMetadata;
 import org.olat.core.commons.services.vfs.VFSRepositoryService;
 import org.olat.core.dispatcher.mapper.Mapper;
@@ -207,7 +209,7 @@ public class MessageListController extends BasicController implements GenericEve
 		mainVC.contextPut("thumbMapper", thumbnailMapper);
 		mainVC.contextPut("guestOnly", new Boolean(guestOnly));
 		
-		messageTableCtrl = new ForumMessageListController(ureq, getWindowControl(), forum, false);
+		messageTableCtrl = new ForumMessageListController(ureq, getWindowControl(), forum, false, true, true);
 		listenTo(messageTableCtrl);
 		mainVC.put("singleThreadTable", messageTableCtrl.getInitialComponent());
 		
@@ -522,9 +524,7 @@ public class MessageListController extends BasicController implements GenericEve
 		if (messages == null || orderedList == null || startMessage == null) return;
 
 		Map<Long, MessageNode> messagesMap = new HashMap<>();
-		if(startMessage != null) {
-			messagesMap.put(startMessage.getKey(), new MessageNode(startMessage));
-		}
+		messagesMap.put(startMessage.getKey(), new MessageNode(startMessage));
 		for(MessageLight message:messages) {
 			if(message.getParentKey() != null) {
 				messagesMap.put(message.getKey(), new MessageNode(message));
@@ -708,6 +708,7 @@ public class MessageListController extends BasicController implements GenericEve
 					getWindowControl().getBusinessControl().getAsString() + "[Message:" + m.getKey() + "]"
 					: currentMark.getBusinessPath();
 			Controller markCtrl = markingService.getMarkController(ureq, getWindowControl(), currentMark, stat, forumOres, keyString, businessPath);
+			listenTo(markCtrl);
 			mainVC.put("mark_".concat(keyString), markCtrl.getInitialComponent());
 		}
 		
@@ -896,6 +897,10 @@ public class MessageListController extends BasicController implements GenericEve
 			if(event instanceof SelectMessageEvent) {
 				SelectMessageEvent sme = (SelectMessageEvent)event;
 				doSelectTheOne(ureq, sme.getMessageKey());
+			} else if (event instanceof MessageMarkedEvent) {
+				MessageMarkedEvent mme = (MessageMarkedEvent) event;
+				Message message = forumManager.getMessageById(mme.getSelectedMessageKey());
+				reloadModel(ureq, message);
 			}
 		} else if(moveCtrl == source) {
 			if(event instanceof SelectMessageEvent) {
@@ -908,6 +913,8 @@ public class MessageListController extends BasicController implements GenericEve
 				MessageView splitedMessage = (MessageView)confirmSplitCtrl.getUserObject();
 				doSplitThread(ureq, splitedMessage);
 			}
+		} else if(event instanceof MarkedEvent || event instanceof UnmarkedEvent) {
+			reloadMessageTable(ureq);
 		} else if(source == cmc) {
 			cleanUp();
 		}
@@ -947,7 +954,7 @@ public class MessageListController extends BasicController implements GenericEve
 			}
 			
 			String reString = "";
-			if(parent != null && parent.isThreadTop()) {
+			if(parent.isThreadTop()) {
 				//add reString only for the first answer
 				reString = translate("msg.title.re");
 			}			
@@ -1242,7 +1249,7 @@ public class MessageListController extends BasicController implements GenericEve
 			case VIEWMODE_MESSAGE: doShowOne(ureq); break;
 			default: doShowAll(ureq);
 		}
-		return viewSettings == null ? VIEWMODE_THREAD : viewSettings;
+		return viewSettings;
 	}
 	
 	private void doShowAll(UserRequest ureq) {
@@ -1281,6 +1288,13 @@ public class MessageListController extends BasicController implements GenericEve
 			messageTableCtrl.loadMessages(new ArrayList<>(backupViews));
 		}
 	}
+
+	private void reloadMessageTable(UserRequest ureq) {
+		String settings = getViewSettings(ureq);
+		if(VIEWMODE_MESSAGE.equals(settings)) {
+			doShowOne(ureq);
+		}
+	}
 	
 	private String getViewSettings(UserRequest ureq) {
 		Preferences prefs = ureq.getUserSession().getGuiPreferences();
@@ -1369,7 +1383,7 @@ public class MessageListController extends BasicController implements GenericEve
 		removeAsListenerAndDispose(cmc);
 		
 		if (foCallback.mayEditMessageAsModerator()) {
-			moveCtrl = new ForumMessageListController(ureq, getWindowControl(), forum, true);
+			moveCtrl = new ForumMessageListController(ureq, getWindowControl(), forum, true, false, false);
 			moveCtrl.loadAllMessages();
 			moveCtrl.setSelectView(message);
 			listenTo(moveCtrl);
diff --git a/src/main/java/org/olat/modules/fo/ui/MessageMarkedEvent.java b/src/main/java/org/olat/modules/fo/ui/MessageMarkedEvent.java
new file mode 100644
index 00000000000..5c9905a7fe0
--- /dev/null
+++ b/src/main/java/org/olat/modules/fo/ui/MessageMarkedEvent.java
@@ -0,0 +1,51 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.fo.ui;
+
+import org.olat.core.gui.control.Event;
+
+/**
+ * 
+ * Initial date: 31 Jul 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class MessageMarkedEvent extends Event {
+
+	private static final long serialVersionUID = -5957284132557840893L;
+
+	private final Long selectedMessageKey;
+	private final Long markedMessageKey;
+
+	public MessageMarkedEvent(Long selectedMessageKey, Long markedMessageKey) {
+		super("message-maked");
+		this.selectedMessageKey = selectedMessageKey;
+		this.markedMessageKey = markedMessageKey;
+	}
+
+	public Long getSelectedMessageKey() {
+		return selectedMessageKey;
+	}
+
+	public Long getMarkedMessageKey() {
+		return markedMessageKey;
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/fo/ui/MessageView.java b/src/main/java/org/olat/modules/fo/ui/MessageView.java
index 55d307d3869..7848a3dc4cd 100644
--- a/src/main/java/org/olat/modules/fo/ui/MessageView.java
+++ b/src/main/java/org/olat/modules/fo/ui/MessageView.java
@@ -54,10 +54,7 @@ public class MessageView extends MessageLightView {
 	private boolean threadTop;
 	private boolean closed;
 	private boolean moved;
-
-	private int depth = -1;
 	
-	private boolean newMessage;
 	private List<VFSItem> attachments;
 	private VFSContainer messageContainer;
 	
@@ -89,14 +86,6 @@ public class MessageView extends MessageLightView {
 		this.formattedLastModified = formattedLastModified;
 	}
 
-	public boolean isNewMessage() {
-		return newMessage;
-	}
-
-	public void setNewMessage(boolean newMessage) {
-		this.newMessage = newMessage;
-	}
-
 	public String getCreatorFirstname() {
 		return creatorFirstname;
 	}
@@ -112,14 +101,6 @@ public class MessageView extends MessageLightView {
 	public void setCreatorLastname(String creatorLastname) {
 		this.creatorLastname = creatorLastname;
 	}
-	
-	public int getDepth() {
-		return depth;
-	}
-
-	public void setDepth(int depth) {
-		this.depth = depth;
-	}
 
 	public boolean isMoved() {
 		return moved;
@@ -169,6 +150,7 @@ public class MessageView extends MessageLightView {
 		this.author = author;
 	}
 
+	@Override
 	public boolean isThreadTop() {
 		return threadTop;
 	}
-- 
GitLab