humhub / fcm-push

Enables your users to receive push notifications from your network.
4 stars 5 forks source link

Notification Grant Status #24

Open luke- opened 1 year ago

luke- commented 1 year ago

It would be good to have a status whether the user has agreed to the notifications or not. Ideally with a possibility to request them again.

A Widget would be cool, which we can add to Notification settings page. https://community.humhub.com/notification/user (I can help to add the Widget from FCM Module to this page)

Options/Output could be:


I'm not sure if it's possible or exactly how it works in conjunction with the Server Worker.

serh-mosk commented 1 year ago

@luke- Unfortunately, it is not possible to receive the confirmation status of receiving push-notifications by the user. Above all, because one user can have several devices. Secondly, push notification technology does not provide for this at the moment. Although Google FCM provides information about the delivery status of a push-notification.

When you send a push-notification using FCM, you receive feedback that contains the message delivery status. This feedback may include information such as:

luke- commented 1 year ago

@yuriimaz It is not about whether a user has received the push notifications. It is about whether the user has agreed to receive them in the current device/browser.

image

E.g. If a user clicked Block we want to show a Message: "You have blocked Notifcations. Click here to enable." If a user clicked Allow we want to show a Message: "Push Notifications are active on this device. Click here to disable."

image

serh-mosk commented 1 year ago

@luke- Sorry, I didn't quite understand and thank you for the clarification. In this case, I will try to figure out if it is possible to use it somehow.

serh-mosk commented 1 year ago

@luke- Since the Notification.permission parameter can have one of three values: 'default', 'granted', and 'denied', and at the same time, a window for changing permissions is possible show only when the 'default' value is set, so I have implemented only the basis of the solution for preliminary testing and coordination. Tested in Chrome, Firefox and Safari. Works well.

It remains to add:

Let me know what you think about these changes and what should be changed.

luke- commented 1 year ago

@yuriimaz This looks really good and promising.

I would prefer:

The existing widget stub https://github.com/humhub/fcm-push/blob/master/widgets/PushNotificationInfoWidget.php should have a view about all different cases (Not Supported, Denied, Granted, Default).

Per Javascript we can hide/show relevant messages.


I can help you then how we can show the widget from the module (FcmPush) in the Core on the Notifications page.

luke- commented 3 months ago

Related: https://github.com/humhub/fcm-push/issues/17

ArchBlood commented 1 month ago

@luke- @serh-mosk maybe this is what you're looking for but displayed under the Notification Overview?

Screenshot_1 Screenshot_2 Screenshot_3

Modified #28

/**
 * @fileoverview HumHub Firebase Module
 * This module handles Firebase Cloud Messaging (FCM) integration for HumHub.
 * It manages notification permissions, token handling, and message reception.
 * @module firebase
 */

humhub.module('firebase', function (module, require, $) {
    let messaging;

    /**
     * Initializes the Firebase module.
     * Sets up the Firebase app if not already initialized and configures message handling.
     */
    const init = function () {
        if (!firebase.apps.length) {
            firebase.initializeApp({messagingSenderId: this.senderId()});
            this.messaging = firebase.messaging();

            this.messaging.onMessage(function (payload) {
                module.log.info("Received FCM Push Notification", payload);
            });

            // Callback fired if Instance ID token is updated.
            this.messaging.onTokenRefresh(function () {
                this.messaging.getToken().then(function (refreshedToken) {
                    this.deleteTokenLocalStore();
                    this.sendTokenToServer(refreshedToken);
                }).catch(function (err) {
                    console.log('Unable to retrieve refreshed token ', err);
                });
            });
        }
    };

    /**
     * Generates the appropriate content for notification permission status.
     * @returns {string} HTML content describing the current notification permission status.
     */
    const getNotificationPermissionContent = function () {
        if (!("Notification" in window)) {
            return 'Not Supported: This browser does not support notifications.';
        }
        console.log('Notification.permission:', Notification.permission);
        switch (Notification.permission) {
            case "granted":
                return 'Granted: Push Notifications are active on this browser.<br>You can disable it in browser settings for this site.';
            case "denied":
                return 'Denied: You have blocked Push Notifications.<br>You can enable it in browser settings for this site.';
            default:
                return 'Default: Push Notifications are not yet enabled.<br><a href="#" id="enablePushBtn"><i class="fa fa-unlock"></i> Click here to enable</a>';
        }
    }

    /**
     * Displays the notification permission request window.
     * Handles the permission request process and updates the UI accordingly.
     */
    const showNotificationPermissionWindow = function () {
        function handlePermission(permission) {
            addPushNotificationPermissionsInfo(permission, true);
        }

        if (!("Notification" in window)) {
            console.log("This browser does not support notifications.");
            handlePermission("not-supported");
        } else {
            Notification.requestPermission().then((permission) => {
                handlePermission(permission);
            });
        }
    }

    /**
     * Adds or updates the push notification permissions information in the UI.
     * @param {string} permission - The current notification permission status.
     * @param {boolean} rewrite - Whether to rewrite existing content or add new content.
     */
    const addPushNotificationPermissionsInfo = function (permission, rewrite = false) {
        if (rewrite) {
            const contentContainer = document.getElementById('notificationPermissions');
            contentContainer.innerHTML = getNotificationPermissionContent()
        } else {
            const content = '<div class="panel panel-default panel-pn-permissions"><div class="panel-body" id="notificationPermissions">' + getNotificationPermissionContent() + '</div></div>';
            $('.layout-sidebar-container').prepend($(content));
        }

        $('#enablePushBtn').on('click', showNotificationPermissionWindow);
    }

    /**
     * Handles tasks after service worker registration.
     * Requests notification permission and manages token retrieval and storage.
     * @param {ServiceWorkerRegistration} registration - The service worker registration object.
     */
    const afterServiceWorkerRegistration = function (registration) {
        const that = this;

        this.messaging.useServiceWorker(registration);

        this.messaging.requestPermission().then(function () {
            addPushNotificationPermissionsInfo('granted');

            that.messaging.getToken().then(function (currentToken) {
                if (currentToken) {
                    that.sendTokenToServer(currentToken);
                } else {
                    module.log.info('No Instance ID token available. Request permission to generate one.');
                    that.deleteTokenLocalStore();
                }
            }).catch(function (err) {
                module.log.error('An error occurred while retrieving token. ', err);
                that.deleteTokenLocalStore();
            });
        }).catch(function (err) {
            module.log.info('Could not get Push Notification permission!', err);
            addPushNotificationPermissionsInfo(Notification.permission);
        });
    };

    /**
     * Sends the FCM token to the server.
     * @param {string} token - The FCM token to be sent.
     */
    const sendTokenToServer = function (token) {
        const that = this;
        if (!that.isTokenSentToServer(token)) {
            module.log.info("Send FCM Push Token to Server");
            $.ajax({
                method: "POST",
                url: that.tokenUpdateUrl(),
                data: {token: token},
                success: function (data) {
                    that.setTokenLocalStore(token);
                }
            });
        } else {
            //console.log('Token already sent to server so won\'t send it again unless it changes');
        }
    };

    /**
     * Checks if the token has been sent to the server.
     * @param {string} token - The token to check.
     * @returns {boolean} True if the token has been sent to the server, false otherwise.
     */
    const isTokenSentToServer = function (token) {
        return (this.getTokenLocalStore() === token);
    };

    /**
     * Deletes the token from local storage.
     */
    const deleteTokenLocalStore = function () {
        window.localStorage.removeItem('fcmPushToken_' + this.senderId())
    };

    /**
     * Stores the token in local storage with an expiry time.
     * @param {string} token - The token to store.
     */
    const setTokenLocalStore = function (token) {
        const item = {
            value: token,
            expiry: (Date.now() / 1000) + (24 * 60 * 60),
        };
        window.localStorage.setItem('fcmPushToken_' + this.senderId(), JSON.stringify(item))
    };

    /**
     * Retrieves the token from local storage.
     * @returns {string|null} The stored token if valid, null otherwise.
     */
    const getTokenLocalStore = function () {
        const itemStr = window.localStorage.getItem('fcmPushToken_' + this.senderId())

        // if the item doesn't exist, return null
        if (!itemStr) {
            return null
        }
        const item = JSON.parse(itemStr)
        const now = (Date.now() / 1000)
        if (now > item.expiry) {
            this.deleteTokenLocalStore();
            return null;
        }
        return item.value;
    };

    /**
     * Returns the URL for updating the token on the server.
     * @returns {string} The token update URL.
     */
    const tokenUpdateUrl = function () {
        return module.config.tokenUpdateUrl;
    };

    /**
     * Returns the sender ID for FCM.
     * @returns {string} The sender ID.
     */
    const senderId = function () {
        return module.config.senderId;
    };

    module.export({
        init: init,
        isTokenSentToServer: isTokenSentToServer,
        sendTokenToServer: sendTokenToServer,
        afterServiceWorkerRegistration: afterServiceWorkerRegistration,
        senderId: senderId,
        tokenUpdateUrl: tokenUpdateUrl,
        setTokenLocalStore: setTokenLocalStore,
        getTokenLocalStore: getTokenLocalStore,
        deleteTokenLocalStore: deleteTokenLocalStore,
    });
});

/**
 * Global function to handle service worker registration for the Firebase module.
 * @param {ServiceWorkerRegistration} registration - The service worker registration object.
 */
function afterServiceWorkerRegistration(registration) {
    humhub.modules.firebase.afterServiceWorkerRegistration(registration);
}
luke- commented 1 month ago

@ArchBlood Thanks, looks goods interesting.

Is it also possible to re-trigger the approval request manually?

Would you like to provide a PR for this?

ArchBlood commented 1 month ago

@ArchBlood Thanks, looks goods interesting.

Is it also possible to re-trigger the approval request manually?

Would you like to provide a PR for this?

Yes, if the settings are reset for some reason it is very possible to re-trigger the approval request manually. I've provided a screenshot for each status that I could produce on my end of things, other than one for no support as I do not know of any browsers that are installable without support.

ArchBlood commented 1 month ago

@luke- see #44

luke- commented 1 month ago

@ArchBlood Awesome thanks! Can you provide the screenshots?

@yurabakhtin Can you please test this behavior?

ArchBlood commented 1 month ago

@ArchBlood Awesome thanks! Can you provide the screenshots?

@yurabakhtin Can you please test this behavior?

Screenshots are attached in the original comment https://github.com/humhub/fcm-push/issues/24#issuecomment-2254208838

yurabakhtin commented 1 month ago

@yurabakhtin Can you please test this behavior?

@luke- I don't see the widget with green border on my test server as displayed on the screenshots https://github.com/humhub/fcm-push/issues/24#issuecomment-2254208838, maybe I should configure the module as described here https://marketplace.humhub.com/module/fcm-push/installation ?

I think all messages like Granted: Push Notifications are active on this browser.<br>You can disable it in browser settings for this site. should be translatable from php side.

ArchBlood commented 1 month ago

@yurabakhtin Can you please test this behavior?

@luke- I don't see the widget with green border on my test server as displayed on the screenshots https://github.com/humhub/fcm-push/issues/24#issuecomment-2254208838, maybe I should configure the module as described here https://marketplace.humhub.com/module/fcm-push/installation ?

I think all messages like Granted: Push Notifications are active on this browser.<br>You can disable it in browser settings for this site. should be translatable from php side.

The border was to highlight the widget nothing more.

yurabakhtin commented 1 month ago

@luke- I think it works as expected, you can see my tests on Windows Chrome browser:

push-notifications


I think all messages like Granted: Push Notifications are active on this browser.<br>You can disable it in browser settings for this site. should be translatable from php side.

@ArchBlood It can be implemented with code:

class FcmPushAsset extends AssetBundle
{
    ...
    public static function register($view)
    {
        ...
            Yii::$app->view->registerJsConfig('firebase', [
                'text' => [
                    'status.granted' => Yii::t('FcmPushModule.base', 'Granted: Push Notifications are active on this browser.<br>You can disable it in browser settings for this site.'),
                ],
            ]);
         ...
    }
}

and replace the JS string with code module.text('status.granted');

ArchBlood commented 1 month ago

@luke- I think it works as expected, you can see my tests on Windows Chrome browser:

push-notifications

I think all messages like Granted: Push Notifications are active on this browser.<br>You can disable it in browser settings for this site. should be translatable from php side.

@ArchBlood It can be implemented with code:

class FcmPushAsset extends AssetBundle
{
    ...
    public static function register($view)
    {
        ...
            Yii::$app->view->registerJsConfig('firebase', [
                'text' => [
                    'status.granted' => Yii::t('FcmPushModule.base', 'Granted: Push Notifications are active on this browser.<br>You can disable it in browser settings for this site.'),
                ],
            ]);
         ...
    }
}

and replace the JS string with code module.text('status.granted');

Done in https://github.com/humhub/fcm-push/pull/44/commits/da92e33866b21a18136cbe88a919565daa55778d & https://github.com/humhub/fcm-push/pull/44/commits/9345b2b76567f86cd5f6e167635476cd4b1f94fe

yurabakhtin commented 1 month ago

Done in da92e33 & 9345b2b

@ArchBlood Thanks, please review the requested changes.

luke- commented 1 month ago

After we want to display the status in the notification settings. We can implement the following:

Or any other idea?

ArchBlood commented 1 month ago

After we want to display the status in the notification settings. We can implement the following:

  • In Core: Add BaseStack Widget "NotificationSettingsInfo"
  • in FCM Push: Add Info Widget to this BaseStack?
  • image

Or any other idea?

We could convert the existing widget to do this or is there any specific use that it currently uses?

https://github.com/humhub/fcm-push/blob/enh/24-notification-grant-status/widgets/PushNotificationInfoWidget.php https://github.com/humhub/fcm-push/blob/enh/24-notification-grant-status/widgets/views/push-notification-info.php

luke- commented 1 month ago

@ArchBlood Yes, we can use it for that!

luke- commented 1 month ago

@ArchBlood Do you want to implement this? Or maybe something for @yurabakhtin ?

ArchBlood commented 1 month ago

@ArchBlood Do you want to implement this? Or maybe something for @yurabakhtin ?

I don't mind doing this, is there s specific design/look that would be required or anything to make it more noticeable?

luke- commented 1 month ago

I think @Eladnarlea will look over it again. At the moment, just a line without a lot of styling is enough.

ArchBlood commented 4 weeks ago

I think @Eladnarlea will look over it again. At the moment, just a line without a lot of styling is enough.

@luke- I wasn't able to implement within /notification/user without core editing which isn't normally recommended, another method would be creating another menu link like this which I did get working without issue;

Red: Haven't found a good method of implementing. Green: Works perfectly.

Screenshot_1

With core editing; Screenshot_2

If you want a better solution then I'm open for other options

luke- commented 4 weeks ago

@ArchBlood Yes, in this case I would make a change in the core develop branch:

If you want, we can also take over the part here?

ArchBlood commented 4 weeks ago

@ArchBlood Yes, in this case I would make a change in the core develop branch:

  • Core: We add an empty widget (BaseStack like sidebar) at this position in the core view (Notification Settings)
  • FCM module: We add the status widget to the empty core widget

If you want, we can also take over the part here?

Sounds fine with me, I'd also like to note that the script will also have to be updated in this case as well, as currently it creates a widget on the dashboard which would have to be refactored in this case as well.

The following are the updated widget and widget view that I currently use;

PushNotificationInfoWidget

Details ```php getModule('fcm-push'); $pushDriver = (new DriverService($module->getConfigureForm()))->getWebDriver(); if ($pushDriver === null) { return ''; } return $this->render('push-notification-info', [ 'tokenUpdateUrl' => Url::to(['/fcm-push/token/update']), 'senderId' => $pushDriver->getSenderId(), ]); } } ```

View

Details ```php Yii::t('FcmPushModule.base', 'Granted: Push Notifications are active on this browser.
You can disable it in browser settings for this site.'), 'denied' => Yii::t('FcmPushModule.base', 'Denied: You have blocked Push Notifications.
You can enable it in browser settings for this site.'), 'default' => Yii::t('FcmPushModule.base', 'Default: Push Notifications are not yet enabled.') . '
' . Button::primary(Yii::t('FcmPushModule.base', 'Click here to enable')) ->icon('fa-unlock') ->id('enablePushBtn') ->sm() ->loader(false), 'not-supported' => Yii::t('FcmPushModule.base', 'Not Supported: This browser does not support notifications.'), ]; $statusTextsJson = json_encode($statusTexts); $this->registerJsConfig('firebase', [ 'tokenUpdateUrl' => $tokenUpdateUrl, 'senderId' => $senderId, ]); $js = <<registerJs($js); ?>
```
luke- commented 4 weeks ago

@ArchBlood should @yurabakhtin take a look into this? Our do you have time on work on this?

ArchBlood commented 4 weeks ago

@ArchBlood should @yurabakhtin take a look into this? Our do you have time on work on this?

I've provide the current widget and widget view that I currently use (see https://github.com/humhub/fcm-push/issues/24#issuecomment-2265901181), I don't have much time to rework the JavaScript sorry.

ArchBlood commented 4 weeks ago

@luke- I believe that everything is in order and complies with everything requested, all that is needed is for review of both P/Rs.

yurabakhtin commented 3 weeks ago

@luke- Some help is required from me here?

I just see you requested "Core: We add an empty widget (BaseStack like sidebar) at this position in the core view (Notification Settings)" and it seems @ArchBlood started it in the PR https://github.com/humhub/humhub/pull/7155 with new core widget NotificationSettingsInfo.

I think we don't need the new widget completely, because there we already the core widget UserInfoWidget which can be used for the second your point "FCM module: We add the status widget to the empty core widget" from the FCM Push module side like this:

config.php:

[UserInfoWidget::class, UserInfoWidget::EVENT_INIT, [Events::class, 'onInitUserInfoWidget']],

Events.php:

public static function onInitUserInfoWidget($event)
{
    /* @var UserInfoWidget $widget */
    $widget = $event->sender;

    $widget->addWidget(PushNotificationInfoWidget::class);
}

I.e. it can be done currently without touching the core, because the core widget UserInfoWidget is available since 1.14 version.

ArchBlood commented 3 weeks ago

@luke- Some help is required from me here?

I just see you requested "Core: We add an empty widget (BaseStack like sidebar) at this position in the core view (Notification Settings)" and it seems @ArchBlood started it in the PR humhub/humhub#7155 with new core widget NotificationSettingsInfo.

I think we don't need the new widget completely, because there we already the core widget UserInfoWidget which can be used for the second your point "FCM module: We add the status widget to the empty core widget" from the FCM Push module side like this:

config.php:

[UserInfoWidget::class, UserInfoWidget::EVENT_INIT, [Events::class, 'onInitUserInfoWidget']],

Events.php:

public static function onInitUserInfoWidget($event)
{
    /* @var UserInfoWidget $widget */
    $widget = $event->sender;

    $widget->addWidget(PushNotificationInfoWidget::class);
}

I.e. it can be done currently without touching the core, because the core widget UserInfoWidget is available since 1.14 version.

I can agree to this method, if it is preferred @luke-?

luke- commented 3 weeks ago

@ArchBlood Please use the widgets suggested by @yurabakhtin. I've already added them for this case, few versions ago.

luke- commented 3 weeks ago

@ArchBlood Also let us know, if yura should finish this task!

ArchBlood commented 3 weeks ago

@luke- done in https://github.com/humhub/fcm-push/pull/44/commits/35a16ccaebcc25a884e568e24eac2f7e382936d2 and https://github.com/humhub/fcm-push/pull/44/commits/15f465780a7a20dc2679cec2b711ab3d70df5a01, although I'd like to note that now the module no longer works because the legacy API has been completely removed and should be completely migrated.

luke- commented 3 weeks ago

@ArchBlood Thanks, can you please rebase the PR or update the base branch. The PR contains many changes which makes it hard to review: https://github.com/humhub/fcm-push/pull/44/files#diff-2bfab3db5cc1baf4c6d3ff6b19901926e3bdf4411ec685dac973e5fcff1c723b

Let's create a new issue for that. https://github.com/humhub/fcm-push/issues/46

ArchBlood commented 3 weeks ago

@ArchBlood Thanks, can you please rebase the PR or update the base branch. The PR contains many changes which makes it hard to review: https://github.com/humhub/fcm-push/pull/44/files#diff-2bfab3db5cc1baf4c6d3ff6b19901926e3bdf4411ec685dac973e5fcff1c723b

Let's create a new issue for that. #46

Done.