diff --git a/component/api/pom.xml b/component/api/pom.xml index cc1963c8843..9c3d336908d 100644 --- a/component/api/pom.xml +++ b/component/api/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-component-api Meeds:: PLF:: Social API diff --git a/component/common/pom.xml b/component/common/pom.xml index d997d1baeb1..e1351d9a3d6 100644 --- a/component/common/pom.xml +++ b/component/common/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT io.meeds.social social-component-common diff --git a/component/core/pom.xml b/component/core/pom.xml index 916070138fc..40f37f5c373 100644 --- a/component/core/pom.xml +++ b/component/core/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-component-core Meeds:: PLF:: Social Core Component diff --git a/component/notification/pom.xml b/component/notification/pom.xml index 2ca9bbaac27..52a57721001 100644 --- a/component/notification/pom.xml +++ b/component/notification/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-component-notification Meeds:: PLF:: Social Notification Component diff --git a/component/notification/src/main/java/io/meeds/social/notification/rest/WebNotificationRestService.java b/component/notification/src/main/java/io/meeds/social/notification/rest/WebNotificationRestService.java index 58a4e46d8aa..cbf4acd9563 100644 --- a/component/notification/src/main/java/io/meeds/social/notification/rest/WebNotificationRestService.java +++ b/component/notification/src/main/java/io/meeds/social/notification/rest/WebNotificationRestService.java @@ -145,6 +145,31 @@ public Response getNotifications( return Response.ok(webNotificationsList).build(); } + @DELETE + @RolesAllowed("users") + @Operation(summary = "Hide notifications", description = "Hides a designated list of notifications", method = "DELETE") + @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Request fullfilled"), + @ApiResponse(responseCode = "400", description = "Invalid query input"), + @ApiResponse(responseCode = "500", description = "Internal server error") }) + public Response hideNotifications( + @Parameter(description = "The list of notification ids to hide", required = true) + @QueryParam("id") + List notificationIds) { + String currentUser = ConversationState.getCurrent().getIdentity().getUserId(); + if (notificationIds == null || notificationIds.isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + for (String notificationId : notificationIds) { + NotificationInfo notification = webNftService.getNotificationInfo(notificationId); + if (notification != null && currentUser.equals(notification.getTo())) { + webNftService.hidePopover(notificationId); + } else { + LOG.warn("User {} is not allowed to hide notification {}", currentUser, notificationId); + } + } + return Response.noContent().build(); + } + @DELETE @Path("{id}") @RolesAllowed("users") @@ -183,6 +208,9 @@ public Response updateNotifications( @Parameter(description = "The list of plugins to include in list", required = false) @QueryParam("plugin") List plugins, + @Parameter(description = "The list of notification ids to update, used by the markAsRead operation", required = false) + @QueryParam("id") + List notificationIds, @Parameter(description = "notification operation", required = true) @QueryParam("operation") String operation) { @@ -192,6 +220,18 @@ public Response updateNotifications( } else if (MARK_ALL_AS_READ_OPERATION.equals(operation)) { webNftService.markAllRead(plugins, currentUser); webNftService.resetNumberOnBadge(plugins, currentUser); + } else if (MARK_AS_READ_OPERATION.equals(operation)) { + if (notificationIds == null || notificationIds.isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + for (String notificationId : notificationIds) { + NotificationInfo notification = webNftService.getNotificationInfo(notificationId); + if (notification != null && currentUser.equals(notification.getTo())) { + webNftService.markRead(notificationId); + } else { + LOG.warn("User {} is not allowed to mark notification {} as read", currentUser, notificationId); + } + } } else { return Response.status(Response.Status.BAD_REQUEST).entity("Unrecognized operation parameter value: " + operation).build(); } diff --git a/component/notification/src/test/java/io/meeds/social/notification/rest/WebNotificationRestServiceTest.java b/component/notification/src/test/java/io/meeds/social/notification/rest/WebNotificationRestServiceTest.java index afe307485d4..ee4b9f884aa 100644 --- a/component/notification/src/test/java/io/meeds/social/notification/rest/WebNotificationRestServiceTest.java +++ b/component/notification/src/test/java/io/meeds/social/notification/rest/WebNotificationRestServiceTest.java @@ -20,8 +20,13 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.List; + import javax.ws.rs.core.Response; import org.mockito.Mock; @@ -113,6 +118,92 @@ public void testAuthorizedHide() { } + public void testBatchMarkAsReadAuthorized() { + startSessionAs("john"); + + webNotificationService = mock(WebNotificationService.class); + NotificationInfo notificationInfo = new NotificationInfo(); + notificationInfo.setTo("john"); + when(webNotificationService.getNotificationInfo(anyString())).thenReturn(notificationInfo); + + WebNotificationRestService webNotificationRestService = newWebNotificationRestService(); + Response response = webNotificationRestService.updateNotifications(null, List.of("1", "2"), "markAsRead"); + + assertEquals(204, response.getStatus()); + verify(webNotificationService).markRead("1"); + verify(webNotificationService).markRead("2"); + } + + public void testBatchMarkAsReadSkipsNotOwned() { + startSessionAs("john"); + + webNotificationService = mock(WebNotificationService.class); + NotificationInfo notificationInfo = new NotificationInfo(); + notificationInfo.setTo("mary"); + when(webNotificationService.getNotificationInfo(anyString())).thenReturn(notificationInfo); + + WebNotificationRestService webNotificationRestService = newWebNotificationRestService(); + Response response = webNotificationRestService.updateNotifications(null, List.of("1"), "markAsRead"); + + assertEquals(204, response.getStatus()); + verify(webNotificationService, never()).markRead(anyString()); + } + + public void testBatchMarkAsReadEmptyIds() { + startSessionAs("john"); + + webNotificationService = mock(WebNotificationService.class); + + WebNotificationRestService webNotificationRestService = newWebNotificationRestService(); + Response response = webNotificationRestService.updateNotifications(null, Collections.emptyList(), "markAsRead"); + + assertEquals(400, response.getStatus()); + verify(webNotificationService, never()).markRead(anyString()); + } + + public void testBatchHideAuthorized() { + startSessionAs("john"); + + webNotificationService = mock(WebNotificationService.class); + NotificationInfo notificationInfo = new NotificationInfo(); + notificationInfo.setTo("john"); + when(webNotificationService.getNotificationInfo(anyString())).thenReturn(notificationInfo); + + WebNotificationRestService webNotificationRestService = newWebNotificationRestService(); + Response response = webNotificationRestService.hideNotifications(List.of("1", "2")); + + assertEquals(204, response.getStatus()); + verify(webNotificationService).hidePopover("1"); + verify(webNotificationService).hidePopover("2"); + } + + public void testBatchHideSkipsNotOwned() { + startSessionAs("john"); + + webNotificationService = mock(WebNotificationService.class); + NotificationInfo notificationInfo = new NotificationInfo(); + notificationInfo.setTo("mary"); + when(webNotificationService.getNotificationInfo(anyString())).thenReturn(notificationInfo); + + WebNotificationRestService webNotificationRestService = newWebNotificationRestService(); + Response response = webNotificationRestService.hideNotifications(List.of("1")); + + assertEquals(204, response.getStatus()); + verify(webNotificationService, never()).hidePopover(anyString()); + } + + public void testBatchHideEmptyIds() { + startSessionAs("john"); + + webNotificationService = mock(WebNotificationService.class); + + WebNotificationRestService webNotificationRestService = newWebNotificationRestService(); + Response response = webNotificationRestService.hideNotifications(Collections. emptyList()); + + assertEquals(400, response.getStatus()); + verify(webNotificationService, never()).hidePopover(anyString()); + } + private void startSessionAs(String username) { Identity identity = new Identity(username); ConversationState state = new ConversationState(identity); diff --git a/component/oauth-auth/pom.xml b/component/oauth-auth/pom.xml index c7bb2a3e80f..18966e414f2 100644 --- a/component/oauth-auth/pom.xml +++ b/component/oauth-auth/pom.xml @@ -21,7 +21,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT 4.0.0 diff --git a/component/pom.xml b/component/pom.xml index b5976751ba8..c851837af89 100644 --- a/component/pom.xml +++ b/component/pom.xml @@ -22,7 +22,7 @@ social io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-component pom diff --git a/component/service/pom.xml b/component/service/pom.xml index c6b52444ca4..166c7ac720d 100644 --- a/component/service/pom.xml +++ b/component/service/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-component-service Meeds:: PLF:: Social Service Component diff --git a/component/web/pom.xml b/component/web/pom.xml index 90866efe91e..e6ece9c6b6a 100644 --- a/component/web/pom.xml +++ b/component/web/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT io.meeds.social social-component-web diff --git a/pom.xml b/pom.xml index ab09c7291e8..8d1b5df5275 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ io.meeds.social social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT pom Meeds:: PLF:: Social Meeds Social - Enterprise Social Networking @@ -45,8 +45,8 @@ - 7.3.x-SNAPSHOT - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT meeds-io diff --git a/webapp/pom.xml b/webapp/pom.xml index 4c6ca6afe37..bf1c5b46eb3 100644 --- a/webapp/pom.xml +++ b/webapp/pom.xml @@ -22,7 +22,7 @@ io.meeds.social social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-webapp war diff --git a/webapp/src/main/resources/locale/portlet/Portlets_en.properties b/webapp/src/main/resources/locale/portlet/Portlets_en.properties index fa07dc1fb56..12c19fe090e 100644 --- a/webapp/src/main/resources/locale/portlet/Portlets_en.properties +++ b/webapp/src/main/resources/locale/portlet/Portlets_en.properties @@ -1066,6 +1066,9 @@ Notification.activity.stopWatching=Stop watching this activity Notification.openInNewWindow=Open in a new tab Notification.markRead=Mark as read Notification.deleteNotification=Delete notification +Notification.select=Select +Notification.label.selectAll=Select all +Notification.label.selectedCount={0} selected Notification.muteSpaceNotification=Mute notifications from this space Notification.tooltip.muteSpaceNotification=Mute this space Notification.tooltip.unmuteSpaceNotification=Unmute this space diff --git a/webapp/src/main/resources/locale/portlet/Portlets_fr.properties b/webapp/src/main/resources/locale/portlet/Portlets_fr.properties index c31d97f8b0c..f2bb5f5ba90 100644 --- a/webapp/src/main/resources/locale/portlet/Portlets_fr.properties +++ b/webapp/src/main/resources/locale/portlet/Portlets_fr.properties @@ -1066,6 +1066,9 @@ Notification.activity.stopWatching=Arrêter de suivre cette activité Notification.openInNewWindow=Ouvrir dans un nouvel onglet Notification.markRead=Marqué comme lu Notification.deleteNotification=Supprimer la notification +Notification.select=Sélectionner +Notification.label.selectAll=Tout sélectionner +Notification.label.selectedCount={0} sélectionné(s) Notification.muteSpaceNotification=Désactiver les notifications issues de cet espace Notification.tooltip.muteSpaceNotification=Mettre en sourdine cet espace Notification.tooltip.unmuteSpaceNotification=Réactiver les notifications de cet espace diff --git a/webapp/src/main/webapp/vue-apps/category-components/components/CategoriesFilter.vue b/webapp/src/main/webapp/vue-apps/category-components/components/CategoriesFilter.vue index dce561f0a85..fd7949f1adc 100644 --- a/webapp/src/main/webapp/vue-apps/category-components/components/CategoriesFilter.vue +++ b/webapp/src/main/webapp/vue-apps/category-components/components/CategoriesFilter.vue @@ -26,7 +26,7 @@ class="flex-column justify-center"> @@ -44,6 +44,7 @@ v-if="displayChipsSelection" :categories="selectedSubcategories" :selected-id="value" + :scrollable="scrollable" class="flex-grow-1 flex-shrink-1 text-start" @select="selectCategory" @open-more="openMore" /> @@ -96,6 +97,12 @@ export default { type: Boolean, default: false, }, + // Lay the filter's chips out in a horizontally scrollable row (mobile behaviour) + // whatever the viewport width, for use in narrow containers such as side drawers. + scrollable: { + type: Boolean, + default: false, + }, }, data: () => ({ categoryTree: null, diff --git a/webapp/src/main/webapp/vue-apps/category-components/components/filter/CategoryChipsGroup.vue b/webapp/src/main/webapp/vue-apps/category-components/components/filter/CategoryChipsGroup.vue index 8c0c37ef295..ec88f27d9e4 100644 --- a/webapp/src/main/webapp/vue-apps/category-components/components/filter/CategoryChipsGroup.vue +++ b/webapp/src/main/webapp/vue-apps/category-components/components/filter/CategoryChipsGroup.vue @@ -22,12 +22,12 @@ --> + + + fa-check-double + + {{ $t('Notification.select') }} + + diff --git a/webapp/src/main/webapp/vue-apps/notification-extensions/js/NotificationService.js b/webapp/src/main/webapp/vue-apps/notification-extensions/js/NotificationService.js index 9e392110ee8..295d424f7a9 100644 --- a/webapp/src/main/webapp/vue-apps/notification-extensions/js/NotificationService.js +++ b/webapp/src/main/webapp/vue-apps/notification-extensions/js/NotificationService.js @@ -103,3 +103,30 @@ export function hideNotification(id) { } }); } + +export function markNotificationsAsRead(ids) { + const params = new URLSearchParams(); + params.append('operation', 'markAsRead'); + (ids || []).forEach(id => params.append('id', id)); + return fetch(`/portal/rest/notifications/webNotifications?${params.toString()}`, { + method: 'PATCH', + credentials: 'include', + }).then((resp) => { + if (!resp.ok) { + throw new Error('Error processing request on server'); + } + }); +} + +export function deleteNotifications(ids) { + const params = new URLSearchParams(); + (ids || []).forEach(id => params.append('id', id)); + return fetch(`/portal/rest/notifications/webNotifications?${params.toString()}`, { + method: 'DELETE', + credentials: 'include', + }).then((resp) => { + if (!resp.ok) { + throw new Error('Error processing request on server'); + } + }); +} diff --git a/webapp/src/main/webapp/vue-apps/notification-top-bar/components/TopBarNotificationDrawer.vue b/webapp/src/main/webapp/vue-apps/notification-top-bar/components/TopBarNotificationDrawer.vue index 9d75a672d8d..f27997cdc08 100644 --- a/webapp/src/main/webapp/vue-apps/notification-top-bar/components/TopBarNotificationDrawer.vue +++ b/webapp/src/main/webapp/vue-apps/notification-top-bar/components/TopBarNotificationDrawer.vue @@ -29,41 +29,72 @@ @closed="$emit('closed')" @expand-updated="expanded = $event">