diff --git a/kernel/api/src/main/java/org/sakaiproject/messaging/api/MessageListener.java b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MessageListener.java deleted file mode 100644 index dfa77ac81e6a..000000000000 --- a/kernel/api/src/main/java/org/sakaiproject/messaging/api/MessageListener.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2003-2021 The Apereo Foundation - * - * Licensed under the Educational Community License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://opensource.org/licenses/ecl2 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sakaiproject.messaging.api; - -import org.sakaiproject.messaging.api.model.UserNotification; - -public interface MessageListener { - - public void read(UserNotification un); -} - diff --git a/kernel/api/src/main/java/org/sakaiproject/messaging/api/UserMessagingService.java b/kernel/api/src/main/java/org/sakaiproject/messaging/api/UserMessagingService.java index deb5a1fe3ff2..84f2ccb8ed27 100644 --- a/kernel/api/src/main/java/org/sakaiproject/messaging/api/UserMessagingService.java +++ b/kernel/api/src/main/java/org/sakaiproject/messaging/api/UserMessagingService.java @@ -15,7 +15,6 @@ */ package org.sakaiproject.messaging.api; -import java.io.InputStream; import java.util.List; import java.util.Map; import java.util.Set; @@ -59,9 +58,6 @@ public interface UserMessagingService { */ boolean importTemplateFromResourceXmlFile(String templateResource, String templateRegistrationKey); - public void listen(String topic, MessageListener listener); - public void send(String topic, UserNotification ba); - /** * @return the list of notifications for the current user */ diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/UserMessagingServiceImpl.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/UserMessagingServiceImpl.java index 24ee858f90e6..ebbfac93aa0e 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/UserMessagingServiceImpl.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/UserMessagingServiceImpl.java @@ -17,7 +17,6 @@ import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.ignite.IgniteMessaging; import org.apache.http.HttpResponse; import org.bouncycastle.jce.ECNamedCurveTable; @@ -67,12 +66,10 @@ import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.event.api.NotificationService; import org.sakaiproject.exception.IdUnusedException; -import org.sakaiproject.ignite.EagerIgniteSpringBean; import org.sakaiproject.messaging.api.model.UserNotification; import org.sakaiproject.messaging.api.UserNotificationData; import org.sakaiproject.messaging.api.UserNotificationHandler; import org.sakaiproject.messaging.api.Message; -import org.sakaiproject.messaging.api.MessageListener; import org.sakaiproject.messaging.api.MessageMedium; import org.sakaiproject.messaging.api.model.PushSubscription; import org.sakaiproject.messaging.api.model.UserNotification; @@ -121,24 +118,27 @@ public class UserMessagingServiceImpl implements UserMessagingService, Observer @Autowired private EmailTemplateService emailTemplateService; @Autowired private EntityManager entityManager; @Autowired private EventTrackingService eventTrackingService; - @Autowired private EagerIgniteSpringBean ignite; @Autowired private PreferencesService preferencesService; @Autowired private PushSubscriptionRepository pushSubscriptionRepository; @Autowired private ServerConfigurationService serverConfigurationService; + @Qualifier("org.sakaiproject.springframework.orm.hibernate.GlobalSessionFactory") @Autowired private SessionFactory sessionFactory; + @Autowired private SessionManager sessionManager; @Autowired private SiteService siteService; @Autowired private ToolManager toolManager; @Autowired private UserDirectoryService userDirectoryService; @Autowired private UserNotificationRepository userNotificationRepository; + @Qualifier("org.sakaiproject.time.api.UserTimeService") @Autowired private UserTimeService userTimeService; + @Setter private ResourceLoader resourceLoader; + @Qualifier("org.sakaiproject.springframework.orm.hibernate.GlobalTransactionManager") @Autowired private PlatformTransactionManager transactionManager; - private IgniteMessaging messaging; private List handlers = new ArrayList<>(); private Map handlerMap = new HashMap<>(); private ExecutorService executor; @@ -161,8 +161,6 @@ public void init() { objectMapper.registerModule(new JavaTimeModule()); - messaging = ignite.message(ignite.cluster().forLocal()); - Security.addProvider(new BouncyCastleProvider()); executor = Executors.newFixedThreadPool(20); @@ -593,20 +591,6 @@ private UserNotification decorateNotification(UserNotification notification) { return notification; } - - public void listen(String topic, MessageListener listener) { - - messaging.localListen(topic, (nodeId, message) -> { - - listener.read(decorateNotification((UserNotification) message)); - return true; - }); - } - - public void send(String topic, UserNotification un) { - messaging.send(topic, un); - } - @Transactional public void subscribeToPush(String endpoint, String auth, String userKey, String browserFingerprint) { diff --git a/library/src/skins/default/src/sass/base/_icons.scss b/library/src/skins/default/src/sass/base/_icons.scss index 82e1f87cd135..b01707b47d67 100644 --- a/library/src/skins/default/src/sass/base/_icons.scss +++ b/library/src/skins/default/src/sass/base/_icons.scss @@ -182,6 +182,7 @@ $fa-font-path: "./fonts"; pin-fill: bi-pin-fill, eye-slash-fill: bi-eye-slash-fill, question: bi-question, + warning: bi-exclamation-lg, ); diff --git a/library/src/skins/default/src/sass/modules/tool/portal/_portal.scss b/library/src/skins/default/src/sass/modules/tool/portal/_portal.scss index 90e191e00a9e..4bee03067a7e 100644 --- a/library/src/skins/default/src/sass/modules/tool/portal/_portal.scss +++ b/library/src/skins/default/src/sass/modules/tool/portal/_portal.scss @@ -241,11 +241,18 @@ .portal-notifications-button { position: relative; } -.portal-notifications-indicator { +.portal-notifications-indicator, .portal-notifications-no-permissions-indicator { position: absolute; - top: 10; + top: 10px; right: 20%; } + +.portal-notifications-no-permissions-indicator { + top: 0px; + right: 23%; + color: red; +} + // make offcanvas header match Sakai's header: .offcanvas-header { flex-shrink: 0; diff --git a/library/src/webapp/js/portal/portal.init.js b/library/src/webapp/js/portal/portal.init.js index 63d1e471fc49..fe627dab11fc 100644 --- a/library/src/webapp/js/portal/portal.init.js +++ b/library/src/webapp/js/portal/portal.init.js @@ -22,4 +22,8 @@ document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll(".portal-search-button").forEach(b => { b.addEventListener("click", () => portal.search.setup({})); }); + + if (portal?.user?.id && Notification?.permission !== "granted") { + document.querySelectorAll(".portal-notifications-no-permissions-indicator").forEach(b => b.classList.remove("d-none")); + } }); diff --git a/library/src/webapp/js/sakai-message-broker.js b/library/src/webapp/js/sakai-message-broker.js deleted file mode 100644 index 8b5b5b493e9d..000000000000 --- a/library/src/webapp/js/sakai-message-broker.js +++ /dev/null @@ -1,182 +0,0 @@ -portal = portal || {}; -portal.notifications = portal.notifications || {}; - -portal.notifications.pushCallbacks = new Map(); - -if (portal?.user?.id) { - - const lastSubscribedUser = localStorage.getItem("last-sakai-user"); - const differentUser = lastSubscribedUser && lastSubscribedUser !== portal.user.id; - - localStorage.setItem("last-sakai-user", portal.user.id); - - if (portal.notifications.pushEnabled && (Notification.permission === "default" || differentUser)) { - - // Permission has neither been granted or denied yet. - - console.debug("No permission set or user changed"); - - navigator.serviceWorker.register("/api/sakai-service-worker.js").then(registration => { - - if (!registration.pushManager) { - // This must be Safari, or maybe IE3 or something :) - console.warn("No pushManager on this registration"); - return; - } - - if (differentUser) { - - registration.pushManager.getSubscription() - .then(subscription => subscription && subscription.unsubscribe()) - .then(() => portal.notifications.subscribeIfPermitted(registration)); - } - - window.addEventListener("DOMContentLoaded", () => { - - console.debug("DOM loaded. Setting up permission triggers ..."); - - // We're using the bullhorn buttons to trigger the permission request from the user. You - // can only instigate a permissions request from a user action. - document.querySelectorAll(".portal-notifications-button").forEach(b => { - - b.addEventListener("click", e => { - - portal.notifications.subscribeIfPermitted(registration); - }, { once: true }); - }); - }); - }); - } - - portal.notifications.subscribeIfPermitted = registration => { - - console.debug("Requesting notifications permission ..."); - - Notification.requestPermission().then(permission => { - - if (permission === "granted") { - - console.debug("Permission granted. Subscribing ..."); - - // We have permission, Grab the public app server key. - fetch("/api/keys/sakaipush").then(r => r.text()).then(key => { - - console.debug("Got the key. Subscribing for push ..."); - - // Subscribe with the public key - registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key }).then(sub => { - - console.debug("Subscribed. Sending details to Sakai ..."); - - const params = { - endpoint: sub.endpoint, - auth: sub.toJSON().keys.auth, - userKey: sub.toJSON().keys.p256dh, - browserFingerprint: getBrowserFingerprint(), - }; - - const url = "/api/users/me/prefs/pushEndpoint"; - fetch(url, { - credentials: "include", - method: "POST", - body: new URLSearchParams(params), - }) - .then(r => { - - if (!r.ok) { - throw new Error(`Network error while posting push endpoint: ${url}`); - } - - console.debug("Subscription details sent successfully"); - }) - .catch (error => console.error(error)); - }); - }); - } - }); - }; - - portal.notifications.setupServiceWorkerListener = () => { - - console.debug("setupServiceWorkerListener"); - - // When the worker's EventSource receives an event it will message us (the client). This - // code looks up the matching callback and calls it. - navigator.serviceWorker.addEventListener('message', e => { - - const allCallbacks = portal.notifications.pushCallbacks.get("all"); - allCallbacks && allCallbacks.forEach(cb => cb(e.data)); - const toolCallbacks = portal.notifications.pushCallbacks.get(e.data.tool); - toolCallbacks && toolCallbacks.forEach(cb => cb(e.data)); - }); - }; - - portal.notifications.registerPushCallback = (toolOrAll, cb) => { - - console.debug(`Registering push callback for ${toolOrAll}`); - - const callbacks = portal.notifications.pushCallbacks.get(toolOrAll) || []; - callbacks.push(cb); - portal.notifications.pushCallbacks.set(toolOrAll, callbacks); - }; - - /** - * Create a promise which will setup the service worker and message registration - * functions before fulfilling. Consumers can wait on this promise and then register - * the push event they want to listen for. For an example of this, checkout - * sui-notifications.js in webcomponents - */ - portal.notifications.setup = new Promise((resolve, reject) => { - - console.debug("Registering worker ..."); - - navigator.serviceWorker.register("/api/sakai-service-worker.js") - .then(registration => { - - const worker = registration.active; - - if (worker) { - - // The serivce worker is already active, setup the listener and register function. - - portal.notifications.setupServiceWorkerListener(); - console.debug("Worker registered and setup"); - resolve(); - } else { - console.debug("No active worker. Waiting for update ..."); - - // Not active. We'll listen for an update then hook things up. - - registration.addEventListener("updatefound", () => { - - console.debug("Worker updated. Waiting for state change ..."); - - const installingWorker = registration.installing; - - installingWorker.addEventListener("statechange", e => { - - console.debug("Worker state changed"); - - if (e.target.state === "activated") { - - console.debug("Worker activated. Setting up ..."); - - // The service worker has been updated, setup the listener and register function. - - portal.notifications.setupServiceWorkerListener(); - console.debug("Worker registered and setup"); - resolve(); - } - }); - }); - } - }) - .catch (error => { - - console.error(`Failed to register service worker ${error}`); - reject(); - }); - }); - - portal.notifications.setup.then(() => console.debug("Notifications setup complete")); -} diff --git a/pom.xml b/pom.xml index 814346594c23..5ab11999e565 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,7 @@ samigo search sections + serviceworker shortenedurl signup simple-rss-portlet diff --git a/portal/portal-impl/impl/src/bundle/sitenav.properties b/portal/portal-impl/impl/src/bundle/sitenav.properties index 898e44f33b55..621bcc4cfde0 100644 --- a/portal/portal-impl/impl/src/bundle/sitenav.properties +++ b/portal/portal-impl/impl/src/bundle/sitenav.properties @@ -54,6 +54,8 @@ sit_notermkey = Courses: No Term sit_moretab_inst = Hidden Sites are not included in this menu. Access them in Home -> Worksite Setup.
To hide a site, go to Home -> Preferences -> Customize Tabs. sit_selectmessage = To operate the combo box, first press Alt+Down Arrow to open it, and then use the up and down arrow keys to scroll through the options. sit_notifications = View your notifications +sit_notifications_not_permitted_label=Notifications have not yet been permitted +sit_new_notifications_label=There are new notifications available sit_navigation = View the site navigation sit_account = View your account details sit_mywor = Home diff --git a/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/SkinnableCharonPortal.java b/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/SkinnableCharonPortal.java index 36c847866e27..7cb173a18c2c 100644 --- a/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/SkinnableCharonPortal.java +++ b/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/SkinnableCharonPortal.java @@ -946,6 +946,8 @@ public PortalRenderContext startPageContext(String siteType, String title, Strin rcontext.put("pageTop", Boolean.valueOf(true)); rcontext.put("rloader", MESSAGES); + rcontext.put("serviceName", serverConfigurationService.getString("ui.service")); + // Allow for inclusion of extra header code via property rcontext.put("includeExtraHead", includeExtraHead); diff --git a/portal/portal-impl/impl/src/java/org/sakaiproject/portal/entityprovider/PortalEntityProvider.java b/portal/portal-impl/impl/src/java/org/sakaiproject/portal/entityprovider/PortalEntityProvider.java index c96ded73a87f..d9df92986099 100644 --- a/portal/portal-impl/impl/src/java/org/sakaiproject/portal/entityprovider/PortalEntityProvider.java +++ b/portal/portal-impl/impl/src/java/org/sakaiproject/portal/entityprovider/PortalEntityProvider.java @@ -146,19 +146,9 @@ public PortalNotifications handleNotify(EntityView view) { @EntityCustomAction(action = "notifications", viewKey = EntityView.VIEW_LIST) public ActionReturn getNotifications(EntityView view) { - String currentUserId = getCheckedCurrentUser(); - - ResourceLoader rl = new ResourceLoader("bullhorns"); - List notifications = userMessagingService.getNotifications(); - - Map data = new HashMap<>(); - data.put("i18n", rl); - - if (notifications.size() > 0) { - data.put("notifications", notifications); - } + getCheckedCurrentUser(); - return new ActionReturn(data); + return new ActionReturn(userMessagingService.getNotifications()); } @EntityCustomAction(action = "clearNotification", viewKey = EntityView.VIEW_LIST) diff --git a/portal/portal-render-engine-impl/impl/src/webapp/vm/morpheus/includeAccountNav.vm b/portal/portal-render-engine-impl/impl/src/webapp/vm/morpheus/includeAccountNav.vm index 2f3f6caacf04..05d3a8fff802 100644 --- a/portal/portal-render-engine-impl/impl/src/webapp/vm/morpheus/includeAccountNav.vm +++ b/portal/portal-render-engine-impl/impl/src/webapp/vm/morpheus/includeAccountNav.vm @@ -85,9 +85,12 @@ For opening help sidebar aria-controls="sakai-notificationsPanel" aria-label="$rloader.sit_notifications" title="$rloader.sit_notifications"> - -