diff --git a/agenda-api/src/main/java/org/exoplatform/agenda/service/AgendaUserSettingsService.java b/agenda-api/src/main/java/org/exoplatform/agenda/service/AgendaUserSettingsService.java index c41e518649..3879568f74 100644 --- a/agenda-api/src/main/java/org/exoplatform/agenda/service/AgendaUserSettingsService.java +++ b/agenda-api/src/main/java/org/exoplatform/agenda/service/AgendaUserSettingsService.java @@ -4,7 +4,6 @@ import org.exoplatform.agenda.model.AgendaUserSettings; import org.exoplatform.agenda.model.EventReminderParameter; -import org.exoplatform.commons.exception.ObjectNotFoundException; import org.exoplatform.social.core.identity.model.Identity; public interface AgendaUserSettingsService { @@ -59,15 +58,6 @@ public interface AgendaUserSettingsService { List getDefaultReminders(); - /** - * Update the user TimeZONE - * - * @param userName userName - * @param timeZone timeZone - * @throws ObjectNotFoundException when user profile is not found - */ - void updateUserTimeZone(String userName, String timeZone) throws ObjectNotFoundException; - /** * Retrieves the globally configured embed map provider identifier. * This setting is shared across all users of the platform. diff --git a/agenda-services/src/main/java/org/exoplatform/agenda/digest/AgendaDigestLinePlugin.java b/agenda-services/src/main/java/org/exoplatform/agenda/digest/AgendaDigestLinePlugin.java new file mode 100644 index 0000000000..cac59028bd --- /dev/null +++ b/agenda-services/src/main/java/org/exoplatform/agenda/digest/AgendaDigestLinePlugin.java @@ -0,0 +1,145 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2026 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.exoplatform.agenda.digest; + +import static org.exoplatform.agenda.util.NotificationUtils.STORED_PARAMETER_EVENT_ID; +import static org.exoplatform.agenda.util.NotificationUtils.STORED_PARAMETER_MODIFIER_IDENTITY_ID; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +import org.apache.commons.lang3.StringUtils; + +import org.exoplatform.agenda.model.Event; +import org.exoplatform.agenda.service.AgendaEventService; +import org.exoplatform.agenda.util.NotificationUtils; +import org.exoplatform.container.ExoContainerContext; +import org.exoplatform.container.xml.InitParams; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.manager.IdentityManager; + +import io.meeds.commons.digest.model.DigestItem; +import io.meeds.commons.digest.model.DigestLine; +import io.meeds.commons.digest.plugin.DigestLineContext; +import io.meeds.commons.digest.plugin.DigestLinePlugin; + +/** + * The digest email lines of the agenda notifications: an invitation to an + * event, a new date poll. The event is read fresh from the stored event id and + * its date is written in the recipient's timezone and language; a deleted + * event gives no line. + */ +public class AgendaDigestLinePlugin extends DigestLinePlugin { + + public static final String EVENT_ADDED_PLUGIN = "EventAddedNotificationPlugin"; + + public static final String DATE_POLL_PLUGIN = "DatePollNotificationPlugin"; + + private static final String LINE_KEY_PREFIX = "digest.line."; + + private static final String ALL_DAY_SUFFIX = ".allDay"; + + private AgendaEventService agendaEventService; + + private IdentityManager identityManager; + + public AgendaDigestLinePlugin(InitParams params) { + super(params); + } + + AgendaDigestLinePlugin(InitParams params, AgendaEventService agendaEventService, IdentityManager identityManager) { + super(params); + this.agendaEventService = agendaEventService; + this.identityManager = identityManager; + } + + @Override + public DigestLine buildLine(DigestItem item, DigestLineContext context) { + Event event = findEvent(item.getParam(STORED_PARAMETER_EVENT_ID)); + if (event == null) { + return null; + } + String title = StringUtils.defaultString(event.getSummary()); + String url = eventUrl(event); + return switch (item.getPluginId()) { + case DATE_POLL_PLUGIN -> DigestLine.of(LINE_KEY_PREFIX + DATE_POLL_PLUGIN, title).withUrl(url); + case EVENT_ADDED_PLUGIN -> invitationLine(item, context, event, title, url); + default -> null; + }; + } + + /** "{actor} invited you to "{title}" — {date} at {time}", date only for an all-day event */ + private DigestLine invitationLine(DigestItem item, DigestLineContext context, Event event, String title, String url) { + String actor = fullName(item.getParam(STORED_PARAMETER_MODIFIER_IDENTITY_ID)); + DateTimeFormatter dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(context.getLocale()); + if (event.getStart() == null) { + return DigestLine.of(LINE_KEY_PREFIX + EVENT_ADDED_PLUGIN + ALL_DAY_SUFFIX, actor, title, "").withUrl(url); + } + if (event.isAllDay()) { + // An all-day event is a calendar day, the same whatever the recipient's + // timezone: never converted, or a westward recipient reads the day before + return DigestLine.of(LINE_KEY_PREFIX + EVENT_ADDED_PLUGIN + ALL_DAY_SUFFIX, actor, title, dateFormat.format(event.getStart().toLocalDate())) + .withUrl(url); + } + ZonedDateTime start = event.getStart().withZoneSameInstant(context.getZoneId()); + String time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(context.getLocale()).format(start); + return DigestLine.of(LINE_KEY_PREFIX + EVENT_ADDED_PLUGIN, actor, title, dateFormat.format(start), time).withUrl(url); + } + + private Event findEvent(String eventId) { + if (StringUtils.isBlank(eventId)) { + return null; + } + try { + return getAgendaEventService().getEventById(Long.parseLong(eventId)); + } catch (NumberFormatException e) { + return null; + } + } + + protected String eventUrl(Event event) { + return NotificationUtils.getEventURL(event); + } + + /** The actor is stored as a social identity id */ + private String fullName(String identityId) { + if (StringUtils.isBlank(identityId)) { + return ""; + } + Identity identity = getIdentityManager().getIdentity(identityId); + String fullName = identity == null || identity.getProfile() == null ? null : identity.getProfile().getFullName(); + return StringUtils.isBlank(fullName) ? "" : fullName; + } + + private AgendaEventService getAgendaEventService() { + if (agendaEventService == null) { + agendaEventService = ExoContainerContext.getService(AgendaEventService.class); + } + return agendaEventService; + } + + private IdentityManager getIdentityManager() { + if (identityManager == null) { + identityManager = ExoContainerContext.getService(IdentityManager.class); + } + return identityManager; + } + +} diff --git a/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/AgendaTemplateBuilder.java b/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/AgendaTemplateBuilder.java index 8e44dee5d8..a9ee2613b0 100644 --- a/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/AgendaTemplateBuilder.java +++ b/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/AgendaTemplateBuilder.java @@ -182,11 +182,6 @@ protected MessageInfo makeMessage(NotificationContext ctx) { } } - @Override - protected boolean makeDigest(NotificationContext notificationContext, Writer writer) { - return false; - } - private final Event getEvent(NotificationInfo notification) { String eventIdString = notification.getValueOwnerParameter("eventId"); if (StringUtils.isBlank(eventIdString)) { diff --git a/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/DatePollNotificationBuilder.java b/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/DatePollNotificationBuilder.java index d4a2ea2cc8..e0331a6fad 100644 --- a/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/DatePollNotificationBuilder.java +++ b/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/DatePollNotificationBuilder.java @@ -2,8 +2,6 @@ import static org.exoplatform.agenda.util.NotificationUtils.*; -import java.io.Writer; - import org.apache.commons.lang3.StringUtils; import org.exoplatform.agenda.model.Event; @@ -102,11 +100,6 @@ protected MessageInfo makeMessage(NotificationContext ctx) { } } - @Override - protected boolean makeDigest(NotificationContext notificationContext, Writer writer) { - return false; - } - public TemplateProvider getTemplateProvider() { return templateProvider; } diff --git a/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/ReminderTemplateBuilder.java b/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/ReminderTemplateBuilder.java index f836b33321..f04bbfe95b 100644 --- a/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/ReminderTemplateBuilder.java +++ b/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/ReminderTemplateBuilder.java @@ -2,7 +2,6 @@ import static org.exoplatform.agenda.util.NotificationUtils.*; -import java.io.Writer; import java.time.ZoneId; import java.time.ZoneOffset; @@ -121,11 +120,6 @@ protected MessageInfo makeMessage(NotificationContext ctx) { } } - @Override - protected boolean makeDigest(NotificationContext notificationContext, Writer writer) { - return false; - } - public TemplateProvider getTemplateProvider() { return templateProvider; } diff --git a/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/ReplyTemplateBuilder.java b/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/ReplyTemplateBuilder.java index 6c7e8b2215..8b025bf4e5 100644 --- a/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/ReplyTemplateBuilder.java +++ b/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/ReplyTemplateBuilder.java @@ -23,7 +23,6 @@ import org.exoplatform.services.log.Log; import org.exoplatform.social.core.manager.IdentityManager; -import java.io.Writer; import java.time.ZoneId; import java.time.ZoneOffset; @@ -116,11 +115,6 @@ protected MessageInfo makeMessage(NotificationContext ctx) { } } - @Override - protected boolean makeDigest(NotificationContext notificationContext, Writer writer) { - return false; - } - public TemplateProvider getTemplateProvider() { return templateProvider; } diff --git a/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/VoteTemplateBuilder.java b/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/VoteTemplateBuilder.java index b4156a184e..605d864836 100644 --- a/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/VoteTemplateBuilder.java +++ b/agenda-services/src/main/java/org/exoplatform/agenda/notification/builder/VoteTemplateBuilder.java @@ -19,7 +19,6 @@ import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.social.core.space.spi.SpaceService; -import java.io.Writer; public class VoteTemplateBuilder extends AbstractTemplateBuilder { @@ -100,11 +99,6 @@ protected MessageInfo makeMessage(NotificationContext ctx) { } } - @Override - protected boolean makeDigest(NotificationContext notificationContext, Writer writer) { - return false; - } - public TemplateProvider getTemplateProvider() { return templateProvider; } diff --git a/agenda-services/src/main/java/org/exoplatform/agenda/notification/plugin/AgendaNotificationPlugin.java b/agenda-services/src/main/java/org/exoplatform/agenda/notification/plugin/AgendaNotificationPlugin.java index 5408e3a6ca..f25b614a88 100644 --- a/agenda-services/src/main/java/org/exoplatform/agenda/notification/plugin/AgendaNotificationPlugin.java +++ b/agenda-services/src/main/java/org/exoplatform/agenda/notification/plugin/AgendaNotificationPlugin.java @@ -2,6 +2,10 @@ import static org.exoplatform.agenda.util.NotificationUtils.*; +import org.exoplatform.agenda.util.Utils; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider; + import java.util.List; import org.apache.commons.lang3.StringUtils; @@ -81,6 +85,13 @@ public NotificationInfo makeNotification(NotificationContext ctx) { notification.key(getId()); if (event.getId() > 0) { setNotificationRecipients(identityManager, notification, spaceService, eventAttendees, event, typeModification, modifierId); + // The one who did the action, so that whoever consumes the notification + // apart from the channels, like the digest, can leave him out of what + // happened to him. The on-site and mail behaviors are unchanged. + Identity modifier = modifierId != null && modifierId > 0 ? Utils.getIdentityById(identityManager, modifierId) : null; + if (modifier != null && OrganizationIdentityProvider.NAME.equals(modifier.getProviderId())) { + notification.setFrom(modifier.getRemoteId()); + } } if (notification.getSendToUserIds() == null || notification.getSendToUserIds().isEmpty()) { LOG.debug("Notification type '{}' doesn't have a recipient", getId()); diff --git a/agenda-services/src/main/java/org/exoplatform/agenda/rest/TimeZoneRest.java b/agenda-services/src/main/java/org/exoplatform/agenda/rest/TimeZoneRest.java deleted file mode 100644 index 7a153b8d4b..0000000000 --- a/agenda-services/src/main/java/org/exoplatform/agenda/rest/TimeZoneRest.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (C) 2026 eXo Platform SAS. - *

- * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program; if not, see. - */ -package org.exoplatform.agenda.rest; - - -import org.exoplatform.agenda.service.AgendaUserSettingsService; -import org.exoplatform.commons.exception.ObjectNotFoundException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.security.access.annotation.Secured; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; - -@RestController -@RequestMapping("timezone") -@Tag(name = "timezone", description = "Update userTimeZone") -public class TimeZoneRest { - @Autowired - private AgendaUserSettingsService agendaUserSettingsService; - - - @PostMapping - @Secured("users") - @Operation(summary = "Update user timeZone", method = "POST", description = "Update user timeZone") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "404", description = "Not found"), - }) - public void updateUserTimeZone( - HttpServletRequest request, - @RequestBody - String timeZone) { - try { - agendaUserSettingsService.updateUserTimeZone(request.getRemoteUser(), timeZone); - } catch (ObjectNotFoundException e) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); - } - } -} diff --git a/agenda-services/src/main/java/org/exoplatform/agenda/service/AgendaUserSettingsServiceImpl.java b/agenda-services/src/main/java/org/exoplatform/agenda/service/AgendaUserSettingsServiceImpl.java index a2e5b40153..edb3bf1d56 100644 --- a/agenda-services/src/main/java/org/exoplatform/agenda/service/AgendaUserSettingsServiceImpl.java +++ b/agenda-services/src/main/java/org/exoplatform/agenda/service/AgendaUserSettingsServiceImpl.java @@ -13,12 +13,8 @@ import org.exoplatform.commons.api.settings.SettingValue; import org.exoplatform.commons.api.settings.data.Context; import org.exoplatform.commons.api.settings.data.Scope; -import org.exoplatform.commons.exception.ObjectNotFoundException; import org.exoplatform.container.xml.InitParams; import org.exoplatform.container.xml.ObjectParameter; -import org.exoplatform.services.organization.OrganizationService; -import org.exoplatform.services.organization.UserProfile; -import org.exoplatform.services.organization.UserProfileHandler; public class AgendaUserSettingsServiceImpl implements AgendaUserSettingsService { @@ -28,8 +24,6 @@ public class AgendaUserSettingsServiceImpl implements AgendaUserSettingsService private static final String AGENDA_USER_SETTING_KEY = "AgendaSettings"; - private static final String TIMEZONE = "user.timeZone"; - private static final String EMBED_MAP_PROVIDER_KEY = "embedMapProvider"; private AgendaEventConferenceService agendaEventConferenceService; @@ -38,8 +32,6 @@ public class AgendaUserSettingsServiceImpl implements AgendaUserSettingsService private SettingService settingService; - private OrganizationService organizationService; - private AgendaUserSettings defaultUserSettings = null; private List defaultReminders = new ArrayList<>(); @@ -47,12 +39,10 @@ public class AgendaUserSettingsServiceImpl implements AgendaUserSettingsService public AgendaUserSettingsServiceImpl(AgendaEventConferenceService agendaEventConferenceService, AgendaRemoteEventService agendaRemoteEventService, SettingService settingService, - OrganizationService organizationService, InitParams initParams) { this.agendaEventConferenceService = agendaEventConferenceService; this.agendaRemoteEventService = agendaRemoteEventService; this.settingService = settingService; - this.organizationService = organizationService; Iterator objectParamIterator = initParams.getObjectParamIterator(); if (objectParamIterator != null) { @@ -156,18 +146,6 @@ public void removeUserConnector(String connectorName, long userIdentityId) { saveAgendaUserSettings(userIdentityId, agendaUserSettings); } - @Override - public void updateUserTimeZone(String userName, String timeZone) throws ObjectNotFoundException { - try { - UserProfileHandler userProfileHandler = organizationService.getUserProfileHandler(); - UserProfile userProfile = userProfileHandler.findUserProfileByName(userName); - userProfile.setAttribute(TIMEZONE, timeZone); - userProfileHandler.saveUserProfile(userProfile, true); - } catch (Exception e) { - throw new ObjectNotFoundException("User profile wasn't found"); - } - } - @Override public List getDefaultReminders() { return Collections.unmodifiableList(defaultReminders); diff --git a/agenda-services/src/main/java/org/exoplatform/agenda/util/NotificationUtils.java b/agenda-services/src/main/java/org/exoplatform/agenda/util/NotificationUtils.java index 4dc38f7fa4..da1aed07d9 100644 --- a/agenda-services/src/main/java/org/exoplatform/agenda/util/NotificationUtils.java +++ b/agenda-services/src/main/java/org/exoplatform/agenda/util/NotificationUtils.java @@ -141,7 +141,7 @@ public class NotificationUtils { public static final String STORED_PARAMETER_EVENT_OWNER_ID = "ownerId"; - private static final String STORED_PARAMETER_EVENT_ID = "eventId"; + public static final String STORED_PARAMETER_EVENT_ID = "eventId"; public static final String STORED_PARAMETER_EVENT_MODIFIER = "eventModifier"; diff --git a/agenda-services/src/test/java/org/exoplatform/agenda/digest/AgendaDigestLinePluginTest.java b/agenda-services/src/test/java/org/exoplatform/agenda/digest/AgendaDigestLinePluginTest.java new file mode 100644 index 0000000000..895a9a32f4 --- /dev/null +++ b/agenda-services/src/test/java/org/exoplatform/agenda/digest/AgendaDigestLinePluginTest.java @@ -0,0 +1,178 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2026 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.exoplatform.agenda.digest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.exoplatform.agenda.model.Event; +import org.exoplatform.agenda.service.AgendaEventService; +import org.exoplatform.container.xml.InitParams; +import org.exoplatform.container.xml.ValuesParam; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.identity.model.Profile; +import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider; +import org.exoplatform.social.core.manager.IdentityManager; + +import io.meeds.commons.digest.model.DigestItem; +import io.meeds.commons.digest.model.DigestLine; +import io.meeds.commons.digest.plugin.DigestLineContext; + +@ExtendWith(MockitoExtension.class) +class AgendaDigestLinePluginTest { + + /** A recipient in Tokyo reading English */ + private static final DigestLineContext CONTEXT = new DigestLineContext("ayoub", Locale.ENGLISH, ZoneId.of("Asia/Tokyo")); + + @Mock + private AgendaEventService agendaEventService; + + @Mock + private IdentityManager identityManager; + + private AgendaDigestLinePlugin plugin; + + @BeforeEach + void setUp() { + InitParams params = new InitParams(); + ValuesParam pluginIds = new ValuesParam(); + pluginIds.setName("pluginIds"); + pluginIds.setValues(new ArrayList<>(List.of(AgendaDigestLinePlugin.EVENT_ADDED_PLUGIN, AgendaDigestLinePlugin.DATE_POLL_PLUGIN))); + params.addParameter(pluginIds); + // The event link needs the running platform: a plain marker here + plugin = new AgendaDigestLinePlugin(params, agendaEventService, identityManager) { + @Override + protected String eventUrl(Event event) { + return "event:" + event.getId(); + } + }; + Identity john = new Identity(OrganizationIdentityProvider.NAME, "john"); + john.setId("15"); + Profile profile = new Profile(john); + profile.setProperty(Profile.FULL_NAME, "John Smith"); + john.setProfile(profile); + lenient().when(identityManager.getIdentity("15")).thenReturn(john); + } + + @Test + void testInvitationLineIsWrittenInTheRecipientTimezone() { + Event event = new Event(); + event.setId(7); + event.setSummary("Sprint review"); + // 10:00 in Paris is 17:00 in Tokyo + event.setStart(ZonedDateTime.parse("2026-09-10T10:00:00+02:00[Europe/Paris]")); + when(agendaEventService.getEventById(7)).thenReturn(event); + + DigestLine line = plugin.buildLine(item(AgendaDigestLinePlugin.EVENT_ADDED_PLUGIN, "eventId", "7", "MODIFIER_IDENTITY_ID", "15"), + CONTEXT); + assertNotNull(line); + assertEquals("digest.line.EventAddedNotificationPlugin", line.getLabelKey()); + assertEquals(List.of("John Smith", "Sprint review", "Sep 10, 2026"), line.getArgs().subList(0, 3)); + // The JDK puts a narrow no-break space before PM + assertEquals("5:00 PM", line.getArgs().get(3).replace(' ', ' ')); + assertEquals("event:7", line.getUrl()); + } + + @Test + void testAllDayInvitationHasNoTime() { + Event event = new Event(); + event.setId(7); + event.setSummary("Company day"); + event.setAllDay(true); + event.setStart(ZonedDateTime.parse("2026-09-10T00:00:00+02:00[Europe/Paris]")); + when(agendaEventService.getEventById(7)).thenReturn(event); + + DigestLine line = plugin.buildLine(item(AgendaDigestLinePlugin.EVENT_ADDED_PLUGIN, "eventId", "7", "MODIFIER_IDENTITY_ID", "15"), + CONTEXT); + assertNotNull(line); + assertEquals("digest.line.EventAddedNotificationPlugin.allDay", line.getLabelKey()); + assertEquals(List.of("John Smith", "Company day", "Sep 10, 2026"), line.getArgs()); + } + + @Test + void testAllDayDateIsTheSameForARecipientWestOfTheEvent() { + Event event = new Event(); + event.setId(7); + event.setSummary("Company day"); + event.setAllDay(true); + event.setStart(ZonedDateTime.parse("2026-09-10T00:00:00+02:00[Europe/Paris]")); + when(agendaEventService.getEventById(7)).thenReturn(event); + DigestLineContext newYork = new DigestLineContext("ayoub", Locale.ENGLISH, ZoneId.of("America/New_York")); + + DigestLine line = plugin.buildLine(item(AgendaDigestLinePlugin.EVENT_ADDED_PLUGIN, "eventId", "7", "MODIFIER_IDENTITY_ID", "15"), + newYork); + assertNotNull(line); + assertEquals("Sep 10, 2026", line.getArgs().get(2)); + } + + @Test + void testDatePollLine() { + Event event = new Event(); + event.setId(8); + event.setSummary("Team lunch"); + when(agendaEventService.getEventById(8)).thenReturn(event); + + DigestLine line = plugin.buildLine(item(AgendaDigestLinePlugin.DATE_POLL_PLUGIN, "eventId", "8", "MODIFIER_IDENTITY_ID", "15"), CONTEXT); + assertNotNull(line); + assertEquals("digest.line.DatePollNotificationPlugin", line.getLabelKey()); + assertEquals(List.of("Team lunch"), line.getArgs()); + } + + @Test + void testDeletedEventGivesNoLine() { + assertNull(plugin.buildLine(item(AgendaDigestLinePlugin.EVENT_ADDED_PLUGIN, "eventId", "404"), CONTEXT)); + assertNull(plugin.buildLine(item(AgendaDigestLinePlugin.EVENT_ADDED_PLUGIN, "eventId", "x"), CONTEXT)); + assertNull(plugin.buildLine(item(AgendaDigestLinePlugin.EVENT_ADDED_PLUGIN), CONTEXT)); + } + + @Test + void testUnknownTypeGivesNoLine() { + Event event = new Event(); + event.setId(7); + when(agendaEventService.getEventById(7)).thenReturn(event); + assertNull(plugin.buildLine(item("EventReminderNotificationPlugin", "eventId", "7"), CONTEXT)); + } + + private static DigestItem item(String pluginId, String... params) { + Map map = new LinkedHashMap<>(); + for (int i = 0; i + 1 < params.length; i += 2) { + map.put(params[i], params[i + 1]); + } + return new DigestItem(1, "ayoub", pluginId, "agenda", Instant.now(), map); + } + +} diff --git a/agenda-services/src/test/java/org/exoplatform/agenda/service/AgendaUserSettingsMultiConnectorTest.java b/agenda-services/src/test/java/org/exoplatform/agenda/service/AgendaUserSettingsMultiConnectorTest.java index 50da8cca07..d7648bd969 100644 --- a/agenda-services/src/test/java/org/exoplatform/agenda/service/AgendaUserSettingsMultiConnectorTest.java +++ b/agenda-services/src/test/java/org/exoplatform/agenda/service/AgendaUserSettingsMultiConnectorTest.java @@ -94,7 +94,6 @@ void setUp() throws Exception { agendaUserSettingsService = new AgendaUserSettingsServiceImpl(agendaEventConferenceService, agendaRemoteEventService, settingService, - organizationService, new InitParams()); lenient().when(agendaRemoteEventService.getRemoteProviders()) .thenReturn(Arrays.asList(new RemoteProvider(1, CALDAV, "apiKey", "secretKey", true, true), diff --git a/agenda-services/src/test/java/org/exoplatform/agenda/service/AgendaUserSettingsServiceTest.java b/agenda-services/src/test/java/org/exoplatform/agenda/service/AgendaUserSettingsServiceTest.java index 5032517923..84ff292c90 100644 --- a/agenda-services/src/test/java/org/exoplatform/agenda/service/AgendaUserSettingsServiceTest.java +++ b/agenda-services/src/test/java/org/exoplatform/agenda/service/AgendaUserSettingsServiceTest.java @@ -27,9 +27,6 @@ import org.exoplatform.agenda.model.EventReminderParameter; import org.exoplatform.agenda.model.RemoteProvider; import org.exoplatform.commons.exception.ObjectNotFoundException; -import org.exoplatform.commons.utils.CommonsUtils; -import org.exoplatform.services.organization.OrganizationService; -import org.exoplatform.services.organization.UserProfile; import org.junit.After; import org.junit.Test; @@ -129,13 +126,6 @@ public void testSaveUserConnector() throws Exception { // NOSONAR } } - @Test - public void testupdateUserTimeZone() throws Exception { // NOSONAR - String timeZone = "UTC"; - agendaUserSettingsService.updateUserTimeZone("testuser1", timeZone); - UserProfile userProfile = CommonsUtils.getService(OrganizationService.class).getUserProfileHandler().findUserProfileByName("testuser1"); - assertEquals("UTC", userProfile.getAttribute("user.timeZone")); - } @Test public void testGetEmbedMapProviderWhenNotSet() { diff --git a/agenda-webapps/src/main/resources/locale/notification/AgendaNotification_en.properties b/agenda-webapps/src/main/resources/locale/notification/AgendaNotification_en.properties index d1fc217ccc..6405c5bf6a 100644 --- a/agenda-webapps/src/main/resources/locale/notification/AgendaNotification_en.properties +++ b/agenda-webapps/src/main/resources/locale/notification/AgendaNotification_en.properties @@ -83,3 +83,10 @@ Notification.label.SayHello=Hi Notification.label.footer=This email was sent to you as eXo Platform member. Click here to change your notification settings. #push Notification.agenda.event.push.created=You are invited to participate to {0} + +# Digest mail notifications: label of the category this addon owns +digest.category.agenda=Agenda +# Digest mail notifications: one line per notification type +digest.line.EventAddedNotificationPlugin={0} invited you to "{1}" \u2014 {2} at {3} +digest.line.EventAddedNotificationPlugin.allDay={0} invited you to "{1}" \u2014 {2} +digest.line.DatePollNotificationPlugin=New date poll: "{0}" diff --git a/agenda-webapps/src/main/webapp/WEB-INF/conf/agenda/notification-configuration.xml b/agenda-webapps/src/main/webapp/WEB-INF/conf/agenda/notification-configuration.xml index bc69374601..d81ba17c04 100644 --- a/agenda-webapps/src/main/webapp/WEB-INF/conf/agenda/notification-configuration.xml +++ b/agenda-webapps/src/main/webapp/WEB-INF/conf/agenda/notification-configuration.xml @@ -376,4 +376,48 @@ + + + + io.meeds.commons.digest.DigestCategoryRegistry + + agenda + addCategoryProvider + io.meeds.commons.digest.plugin.DigestCategoryPlugin + The agenda notifications: event invitations and date polls + + + id + agenda + + + labelKey + digest.category.agenda + + + order + 50 + + + pluginIds + EventAddedNotificationPlugin + DatePollNotificationPlugin + + + + + + agenda.digest.lines + addLineProvider + org.exoplatform.agenda.digest.AgendaDigestLinePlugin + Builds the digest email lines of the event invitations and date polls + + + pluginIds + EventAddedNotificationPlugin + DatePollNotificationPlugin + + + + diff --git a/agenda-webapps/src/main/webapp/WEB-INF/conf/agenda/webui-configuration.xml b/agenda-webapps/src/main/webapp/WEB-INF/conf/agenda/webui-configuration.xml deleted file mode 100644 index 39b60444e6..0000000000 --- a/agenda-webapps/src/main/webapp/WEB-INF/conf/agenda/webui-configuration.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - org.exoplatform.groovyscript.text.TemplateService - - UIPortalApplication-head - addTemplateExtension - org.exoplatform.groovyscript.text.TemplateExtensionPlugin - - - templates - war:/groovy/UIPortalAgendaHead.gtmpl - - - - - - diff --git a/agenda-webapps/src/main/webapp/WEB-INF/conf/configuration.xml b/agenda-webapps/src/main/webapp/WEB-INF/conf/configuration.xml index 6594ab5317..2f6d9b014e 100644 --- a/agenda-webapps/src/main/webapp/WEB-INF/conf/configuration.xml +++ b/agenda-webapps/src/main/webapp/WEB-INF/conf/configuration.xml @@ -12,6 +12,5 @@ war:/conf/agenda/cache-configuration.xml war:/conf/agenda/upgrade-plugins-configuration.xml war:/conf/agenda/websocket-configuration.xml - war:/conf/agenda/webui-configuration.xml war:/conf/agenda/metadata-plugins-configuration.xml diff --git a/agenda-webapps/src/main/webapp/WEB-INF/gatein-resources.xml b/agenda-webapps/src/main/webapp/WEB-INF/gatein-resources.xml index 1713476714..a5213b9b2d 100644 --- a/agenda-webapps/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/agenda-webapps/src/main/webapp/WEB-INF/gatein-resources.xml @@ -269,13 +269,6 @@ extensionRegistry - - agendaBaseExtension - baseGRP - - AgendaSpaceSettingExtension SpaceSettingExtensions diff --git a/agenda-webapps/src/main/webapp/groovy/UIPortalAgendaHead.gtmpl b/agenda-webapps/src/main/webapp/groovy/UIPortalAgendaHead.gtmpl deleted file mode 100644 index 4b35db52ad..0000000000 --- a/agenda-webapps/src/main/webapp/groovy/UIPortalAgendaHead.gtmpl +++ /dev/null @@ -1,14 +0,0 @@ -<% - import org.exoplatform.container.ExoContainerContext; - import org.exoplatform.services.security.ConversationState; - import org.exoplatform.services.organization.OrganizationService; - import org.exoplatform.services.organization.UserProfile; - - OrganizationService organizationService = ExoContainerContext.getService(OrganizationService.class); - String userName = ConversationState.getCurrent().getIdentity().getUserId(); - UserProfile userProfile = organizationService.getUserProfileHandler().findUserProfileByName(userName); - String userTimezone = userProfile == null ? null : userProfile.getAttribute("user.timeZone"); - -%> \ No newline at end of file diff --git a/agenda-webapps/src/main/webapp/vue-app/agenda-base-extension/main.js b/agenda-webapps/src/main/webapp/vue-app/agenda-base-extension/main.js deleted file mode 100644 index 00fe6e92c6..0000000000 --- a/agenda-webapps/src/main/webapp/vue-app/agenda-base-extension/main.js +++ /dev/null @@ -1,15 +0,0 @@ -const timeZoneId = new window.Intl.DateTimeFormat().resolvedOptions().timeZone; -if (eXo.env.portal.userName && eXo.env.portal.userTimezone !== timeZoneId) { - fetch('/agenda/rest/timezone', { - headers: { - 'Content-Type': 'text/plain', - }, - method: 'POST', - credentials: 'include', - body: timeZoneId, - }).then(resp => { - if (!resp || !resp.ok) { - throw new Error('Server Request Error: Cannot update user TimeZone'); - } - }); -} \ No newline at end of file diff --git a/agenda-webapps/webpack.prod.js b/agenda-webapps/webpack.prod.js index 70bf34521c..e45931224a 100644 --- a/agenda-webapps/webpack.prod.js +++ b/agenda-webapps/webpack.prod.js @@ -32,7 +32,6 @@ const config = { agendaNotificationsExtension: './src/main/webapp/vue-app/agenda-notifications/main.js', engagementCenterExtensions: './src/main/webapp/vue-app/engagementCenterExtensions/extensions.js', agendaEventContentLinkExtension: './src/main/webapp/vue-app/content-link/extensions.js', - agendaBaseExtension: './src/main/webapp/vue-app/agenda-base-extension/main.js', agendaSpaceAdministration: './src/main/webapp/vue-app/agenda-space-administration/main.js', contentPublishExtensions: './src/main/webapp/vue-app/agenda-extensions/content-publication-extensions/main.js', eventActivityStreamExtensions: './src/main/webapp/vue-app/agenda-extensions/activity-stream-extensions/main.js',