I am working on a project that uses vaadin as frontend and spring-boot for the backend. In this application I have a certain usecase where a group of users (operators) work on different stations where they can generate events that another different group of users (supevisors) should be notified about - but only for the stations that they are assigned to. Generally not a big deal but I want to use web push notifications to notify the supervisors once a new event happens.
I started reading a bit about the Notification API and managed to create a View in vaadin that checks whether the browser was granted the permission to send notification (in my example on windows 10) and if the permission isn't granted yet to display a button to ask for it. if the permission was granted the button will be hidden and a label confirms that permission was granted. simultaneously a test notification is sent (for me to test if it works). Everything works perfectly so far.
I implemented everything using a mix of java/vaadin and native java script
window.sendNotification = function(element, body, icon, title, type, id) {
var options = {
body: body,
icon: icon
}
var notification = new Notification(title, options);
notification.onclick = (e) => element.$server.notificationClicked(type, id);
}
window.checkNotificationPermission = function() {
return Notification.permission;
}
window.askNotificationPermission = function(element) {
function handlePermission(permission) {
element.$server.permissionAsked(permission);
}
// Let's check if the browser supports notifications
if (!('Notification' in window)) {
console.log("This browser does not support notifications.");
} else {
if (checkNotificationPromise()) {
Notification.requestPermission()
.then((permission) => {
handlePermission(permission);
})
} else {
Notification.requestPermission(function(permission) {
handlePermission(permission);
});
}
}
}
function checkNotificationPromise() {
// safari supported permission or all other browsers?
try {
Notification.requestPermission().then();
} catch (e) {
return false;
}
return true;
}
This is the js side, in java/vaadin i annotated the view with @PWA (using the generated sw.js and manifest), @Push and @JsModule to include the js code. additionally i call each js function using page.executeJs(), for example like this:
Page page = UI.getCurrent().getPage();
page.executeJs("askNotificationPermission($0)", this.getElement());
i also provide callback methods which handle when permission was granted or when a notification was clicked. for example:
@ClientCallable
private void permissionAsked(String permission) {
sendSuccessNotification();
permissionChecked(permission);
}
until here everything worked fine.
Now my big problem is how I can automatically notify each user (some of them might even be logged in on different machines, for example a PC and a smartphone). I thought of letting another thread run on the view and letting it poll the backend for new notifications:
like this:
@Override
protected void onAttach(AttachEvent attachEvent) {
// Start the data feed thread
thread = new NotificationThread();
thread.start();
}
@Override
protected void onDetach(DetachEvent detachEvent) {
// Cleanup
thread.interrupt();
thread = null;
}
private class NotificationThread extends Thread {
@Override
public void run() {
try {
while (true) {
Thread.sleep(10000); // update every 10 seconds
List<ResponseNotification> findNewNotifications = notificationController.findNewNotifications();
if (findNewNotifications.size() > 1) {
sendGeneralNotification("You have new notifications", "New Notifications!", NavigationTarget.NOTIFICATIONS, "");
} else if (findNewNotifications.size() == 1) {
sendGeneralNotification("An operator requires your attention", "Operator", NavigationTarget.OPERATOR, notification.operator.id);
}
}
} catch (InterruptedException e) {
// ...
// handle
}
}
}
The biggest issue in this approach is that the authentication is not available anymore in the backend, as it was called from another thread and therefore SecurityContextHolder.getContext().getAuthentication()
would return null.
But i am also not sure if the general approach is reasonable, since the view always needs to be open, and i feel like polling every 10 seconds might bring some unnecessary load to the backend once there's more than only a bunch of supervisors.
the events are stored in my database and i also want to send an email in addition to the notification so it would be perfect if there was a way to send notifications from the backend and pushed to the clients while the events are handled anyway. maybe someone already has experience and can help me to find a solution to this problem .
thanks in advance!
If you need to notify users even when the application is not open, you need to look in to Web Push. Unfortunately this requires a custom (but similar) solution on Safari, and is not available on iOS, which is the main reason Vaadin does not have a built-in solution yet. Marcus Hellberg made a prototype some years back that might provide some hints.
To notify users that currently have the applications open, consider if this might be applicable in your case: Instead of each user polling for relevant events every 10 seconds, consider “subscribing” to events regarding each station the user is interested in, then dispatch an “event” to all subscribers as the event happens. Users will only stay subscribed as long as they are logged in, so the publishing thread does not need to keep track of permissions.
This requires you to use some sort of event bus for the pub/sub mechanism, and for this you could a look at Collaboration Engine - mainly because it's integrated with Vaadin, and brings other collaborative features you might find useful (maybe a log of events, or a chat at each station so each operator can add some extra details). It's most powerful when you use the high-level APIs and components, but you can use the low-level API to do a lot of nifty things.
There are multiple ways you could set that up, but for instance create a topic called “notifications”, use a CollaborativeMap to share updates for each station. (Or you could create a separate topic for each station, depending on your needs.)
Something like this for each user:
CollaborationEngine.getInstance().openTopicConnection(this, "notifications", localUser, topic -> {
CollaborationMap stations = topic.getNamedMap("stations");
return stations.subscribe(event -> {
if (MY_STATIONS.contains(event.getKey())) {
Notification notification = new Notification(event.getValue(String.class));
notification.setDuration(5000);
notification.open();
}
});
});
Then when there is an update about a station, get the stationMap in the same way, but update it:
stationMap.put(STATION_ID, STATION_ID + “: An update at " + new Date());
This is of course bordering on pseudocode-brief, as this might not be exactly what you are looking for, but the code did produce a working example: