diff --git a/README.md b/README.md
index f5dde1b..56d9ce1 100644
--- a/README.md
+++ b/README.md
@@ -10,14 +10,12 @@ Notifications are short messages that notify users of something that occurred in
Requirements
------------
-- PHP 7.1+
+- PHP 8.1+
- gmp
- mbstring
- curl
- openssl
-- PHP 7.2+ is recommended for better performance.
-
Installation
------------
@@ -316,4 +314,4 @@ If you customize the HTML template remember to include a button with id 'js-web-
```
-Remember to place the service-worker.js file in the web root in order to serve the service worker when the WebNotifications widget is initialized.
\ No newline at end of file
+Remember to place the service-worker.js file in the web root in order to serve the service worker when the WebNotifications widget is initialized.
diff --git a/assets/js/notifications.js b/assets/js/notifications.js
index 0621a44..1f7403c 100644
--- a/assets/js/notifications.js
+++ b/assets/js/notifications.js
@@ -1,36 +1,37 @@
+'use strict';
+
/**
* notifications plugin
*/
-
-var Notifications = (function(opts) {
- if(!opts.id){
+const Notifications = (function (opts) {
+ if (!opts.id) {
throw new Error('Notifications: the param id is required.');
}
- var elem = $('#'+opts.id);
- if(!elem.length){
+ const elem = $('#' + opts.id);
+ if (!elem.length) {
throw Error('Notifications: the element was not found.');
}
- var options = $.extend({
+ const options = $.extend({
pollInterval: 60000,
xhrTimeout: 2000,
- readLabel: 'read',
+ readLabel: 'mark as unread',
markAsReadLabel: 'mark as read'
}, opts);
/**
- * Renders a notification row
+ * Renders a notification row.
*
* @param object The notification instance
* @returns {jQuery|HTMLElement|*}
*/
- var renderRow = function (object) {
- var html = '
' +
- ' '+
+ ' ' +
'' + object.message + '' +
'' + object.timeago + '' +
'' +
@@ -38,58 +39,73 @@ var Notifications = (function(opts) {
return $(html);
};
- var showList = function() {
- var list = elem.find('.notifications-list');
+ /**
+ * Initialise the "mark as read"/"mark as unread" buttons.
+ */
+ const initMarkReadButtons = function () {
+ $('.notifications-list').find('.mark-read').off('click').on('click', function (e) {
+ e.stopPropagation();
+ const item = $(this).closest('.notification-item');
+ let url = opts.readUrl;
+ let callback = displayAsRead;
+ if (item.hasClass('read')) {
+ url = opts.unreadUrl;
+ callback = displayAsUnread;
+ }
+ const mark = $(this);
+ $.ajax({
+ url: url,
+ type: 'GET',
+ data: {id: item.data('id')},
+ dataType: 'json',
+ timeout: opts.xhrTimeout,
+ success: function (data) {
+ callback(mark);
+ }
+ });
+ }).tooltip('dispose').tooltip();
+ };
+
+ /**
+ * Render out the full list of notifications.
+ */
+ const showList = function () {
+ let list = elem.find('.notifications-list');
$.ajax({
url: options.url,
- type: "GET",
- dataType: "json",
+ type: 'GET',
+ dataType: 'json',
timeout: opts.xhrTimeout,
//loader: list.parent(),
- success: function(data) {
- var seen = 0;
+ success: function (data) {
+ let seen = 0;
- if($.isEmptyObject(data.list)){
+ if ($.isEmptyObject(data.list)) {
list.find('.empty-row span').show();
}
$.each(data.list, function (index, object) {
- if(list.find('>div[data-id="' + object.id + '"]').length){
+ if (list.find('>div[data-id="' + object.id + '"]').length) {
return;
}
- var item = renderRow(object);
- item.find('.mark-read').on('click', function(e) {
- e.stopPropagation();
- if(item.hasClass('read')){
- return;
- }
- var mark = $(this);
- $.ajax({
- url: options.readUrl,
- type: "GET",
- data: {id: item.data('id')},
- dataType: "json",
- timeout: opts.xhrTimeout,
- success: function (data) {
- markRead(mark);
- }
- });
- }).tooltip();
+ let item = renderRow(object);
- if(object.url){
- item.on('click', function(e) {
+ if (object.url) {
+ item.on('click', function (e) {
document.location = object.url;
});
}
- if(object.seen == '0'){
+ if (object.seen == '0') {
seen += 1;
}
list.append(item);
});
+ initMarkReadButtons();
+
setCount(seen, true);
startPoll(true);
@@ -97,75 +113,89 @@ var Notifications = (function(opts) {
});
};
- elem.find('> a[data-toggle="dropdown"]').on('click', function(e){
- if(!$(this).parent().hasClass('show')){
+ elem.find('> a[data-toggle="dropdown"]').on('click', function (e) {
+ if (!$(this).parent().hasClass('show')) {
showList();
}
});
- elem.find('.read-all').on('click', function(e){
+ elem.find('.read-all').on('click', function (e) {
e.stopPropagation();
- var link = $(this);
+ let link = $(this);
$.ajax({
url: options.readAllUrl,
- type: "GET",
- dataType: "json",
+ type: 'GET',
+ dataType: 'json',
timeout: opts.xhrTimeout,
success: function (data) {
- markRead(elem.find('.dropdown-item:not(.read)').find('.mark-read'));
- link.off('click').on('click', function(){ return false; });
+ displayAsRead(elem.find('.dropdown-item:not(.read)').find('.mark-read'));
+ link.off('click').on('click', function () {
+ return false;
+ });
updateCount();
}
});
});
- var markRead = function(mark){
- mark.off('click').on('click', function(){ return false; });
+ /**
+ * Mark a notification as read.
+ * @param mark
+ */
+ const displayAsRead = function (mark) {
mark.attr('title', options.readLabel);
mark.tooltip('dispose').tooltip();
- mark.closest('.dropdown-item').addClass('read');
+ mark.closest('.notification-item').addClass('read');
+ };
+
+ /**
+ * Mark a notification as unread.
+ * @param mark
+ */
+ const displayAsUnread = function (mark) {
+ mark.attr('title', options.markAsReadLabel);
+ mark.tooltip('dispose').tooltip();
+ mark.closest('.notification-item').removeClass('read');
};
- var setCount = function(count, decrement) {
- var badge = elem.find('.notifications-count');
- if(decrement) {
+ const setCount = function (count, decrement) {
+ const badge = elem.find('.notifications-count');
+ if (decrement) {
count = parseInt(badge.data('count')) - count;
}
- if(count > 0){
+ if (count > 0) {
badge.data('count', count).text(count).show();
- }
- else {
+ } else {
badge.data('count', 0).text(0).hide();
}
};
- var updateCount = function() {
+ const updateCount = function () {
$.ajax({
url: options.countUrl,
- type: "GET",
- dataType: "json",
+ type: 'GET',
+ dataType: 'json',
timeout: opts.xhrTimeout,
- success: function(data) {
+ success: function (data) {
setCount(data.count);
},
- complete: function() {
+ complete: function () {
startPoll();
}
});
};
- var _updateTimeout;
- var startPoll = function(restart) {
- if (restart && _updateTimeout){
+ let _updateTimeout;
+ const startPoll = function (restart) {
+ if (restart && _updateTimeout) {
clearTimeout(_updateTimeout);
}
- _updateTimeout = setTimeout(function() {
+ _updateTimeout = setTimeout(function () {
updateCount();
}, opts.pollInterval);
};
// Fire the initial poll
startPoll();
-
+ initMarkReadButtons();
});
\ No newline at end of file
diff --git a/composer.json b/composer.json
index ab802e8..c659247 100644
--- a/composer.json
+++ b/composer.json
@@ -18,9 +18,9 @@
],
"license": "BSD-3-Clause",
"require": {
- "php": ">=7.1",
+ "php": ">=8.1",
"yiisoft/yii2": "~2.0.13",
- "minishlink/web-push": "^5.2"
+ "minishlink/web-push": "^8.0.0"
},
"autoload": {
"psr-4": {
@@ -35,5 +35,10 @@
"type": "composer",
"url": "https://asset-packagist.org"
}
- ]
-}
\ No newline at end of file
+ ],
+ "config": {
+ "allow-plugins": {
+ "yiisoft/yii2-composer": true
+ }
+ }
+}
diff --git a/controllers/DefaultController.php b/controllers/DefaultController.php
index 7da202f..999a84b 100644
--- a/controllers/DefaultController.php
+++ b/controllers/DefaultController.php
@@ -3,6 +3,7 @@
namespace webzop\notifications\controllers;
use Yii;
+use yii\db\Exception;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\db\Query;
@@ -10,10 +11,13 @@
use yii\helpers\Url;
use webzop\notifications\helpers\TimeElapsed;
use webzop\notifications\widgets\Notifications;
+use yii\web\Response;
class DefaultController extends Controller
{
-
+ /**
+ * @inheritdoc
+ */
public function behaviors()
{
return [
@@ -33,7 +37,6 @@ public function behaviors()
/**
* Displays index page.
- *
* @return string
*/
public function actionIndex()
@@ -43,10 +46,12 @@ public function actionIndex()
->from('{{%notifications}}')
->andWhere(['or', 'user_id = 0', 'user_id = :user_id'], [':user_id' => $userId]);
- $pagination = new Pagination([
- 'pageSize' => 20,
- 'totalCount' => $query->count(),
- ]);
+ $pagination = new Pagination(
+ [
+ 'pageSize' => 20,
+ 'totalCount' => $query->count(),
+ ]
+ );
$list = $query
->orderBy(['id' => SORT_DESC])
@@ -56,12 +61,19 @@ public function actionIndex()
$notifs = $this->prepareNotifications($list);
- return $this->render('index', [
- 'notifications' => $notifs,
- 'pagination' => $pagination,
- ]);
+ return $this->render(
+ 'index',
+ [
+ 'notifications' => $notifs,
+ 'pagination' => $pagination,
+ ]
+ );
}
+ /**
+ * Get a list of all notifications.
+ * @return Response
+ */
public function actionList()
{
$userId = Yii::$app->getUser()->getId();
@@ -72,34 +84,69 @@ public function actionList()
->limit(10)
->all();
$notifs = $this->prepareNotifications($list);
- $this->ajaxResponse(['list' => $notifs]);
+ return $this->ajaxResponse(['list' => $notifs]);
}
+ /**
+ * Get the amount of unseen notifications.
+ * @return Response
+ */
public function actionCount()
{
$count = Notifications::getCountUnseen();
- $this->ajaxResponse(['count' => $count]);
+ return $this->ajaxResponse(['count' => $count]);
}
+ /**
+ * Mark a notification as read.
+ * @param $id
+ * @return Response
+ * @throws \yii\base\InvalidRouteException
+ * @throws \yii\db\Exception
+ */
public function actionRead($id)
{
Yii::$app->getDb()->createCommand()->update('{{%notifications}}', ['read' => true], ['id' => $id])->execute();
- if(Yii::$app->getRequest()->getIsAjax()){
+ if (Yii::$app->getRequest()->getIsAjax()) {
+ return $this->ajaxResponse(1);
+ }
+
+ return Yii::$app->getResponse()->redirect(['/notifications/default/index']);
+ }
+
+ /**
+ * Mark a notification as unread.
+ * @param $id
+ * @return Response
+ * @throws \yii\base\InvalidRouteException
+ * @throws \yii\db\Exception
+ */
+ public function actionUnread($id)
+ {
+ Yii::$app->getDb()->createCommand()->update('{{%notifications}}', ['read' => false], ['id' => $id])->execute();
+
+ if (Yii::$app->getRequest()->getIsAjax()) {
return $this->ajaxResponse(1);
}
return Yii::$app->getResponse()->redirect(['/notifications/default/index']);
}
+ /**
+ * Mark all notifications as read.
+ * @return Response
+ * @throws \yii\base\InvalidRouteException
+ * @throws \yii\db\Exception
+ */
public function actionReadAll()
{
Yii::$app->getDb()->createCommand()->update(
'{{%notifications}}',
['read' => true, 'seen' => true],
['user_id' => Yii::$app->user->id]
- )->execute();
- if(Yii::$app->getRequest()->getIsAjax()){
+ )->execute();
+ if (Yii::$app->getRequest()->getIsAjax()) {
return $this->ajaxResponse(1);
}
@@ -108,11 +155,17 @@ public function actionReadAll()
return Yii::$app->getResponse()->redirect(['/notifications/default/index']);
}
+ /**
+ * Delete all notifications.
+ * @return Response
+ * @throws \yii\base\InvalidRouteException
+ * @throws \yii\db\Exception
+ */
public function actionDeleteAll()
{
Yii::$app->getDb()->createCommand()->delete('{{%notifications}}')->execute();
- if(Yii::$app->getRequest()->getIsAjax()){
+ if (Yii::$app->getRequest()->getIsAjax()) {
return $this->ajaxResponse(1);
}
@@ -121,11 +174,19 @@ public function actionDeleteAll()
return Yii::$app->getResponse()->redirect(['/notifications/default/index']);
}
- private function prepareNotifications($list){
+ /**
+ * Create an array of the given notifications as returned by /list
+ * and mark them all as seen.
+ * @param array $list
+ * @return array
+ * @throws Exception
+ */
+ private function prepareNotifications($list)
+ {
$notifs = [];
$seen = [];
- foreach($list as $notif){
- if(!$notif['seen']){
+ foreach ($list as $notif) {
+ if (!$notif['seen']) {
$seen[] = $notif['id'];
}
$route = @unserialize($notif['route']);
@@ -134,7 +195,7 @@ private function prepareNotifications($list){
$notifs[] = $notif;
}
- if(!empty($seen)){
+ if (!empty($seen)) {
Yii::$app->getDb()->createCommand()->update('{{%notifications}}', ['seen' => true], ['id' => $seen])->execute();
}
@@ -143,7 +204,7 @@ private function prepareNotifications($list){
public function ajaxResponse($data = [])
{
- if(is_string($data)){
+ if (is_string($data)) {
$data = ['html' => $data];
}
@@ -151,7 +212,7 @@ public function ajaxResponse($data = [])
$flashes = $session->getAllFlashes(true);
foreach ($flashes as $type => $message) {
$data['notifications'][] = [
- 'type' => $type,
+ 'type' => $type,
'message' => $message,
];
}
diff --git a/controllers/WebPushNotificationController.php b/controllers/WebPushNotificationController.php
index 11f1426..3ae3cc6 100644
--- a/controllers/WebPushNotificationController.php
+++ b/controllers/WebPushNotificationController.php
@@ -18,12 +18,12 @@ public function behaviors()
'class' => AccessControl::class,
'rules' => [
[
- 'allow' => true,
- 'roles' => ['@'],
+ 'allow' => true,
+ 'roles' => ['@'],
'actions' => ['subscribe', 'unsubscribe'],
],
[
- 'allow' => true,
+ 'allow' => true,
'actions' => ['service-worker']
],
]
@@ -49,18 +49,18 @@ public function beforeAction($action)
public function actionSubscribe()
{
$userId = null;
- if(Yii::$app->getUser()) {
+ if (Yii::$app->getUser()) {
$userId = Yii::$app->getUser()->getId();
}
$request = Yii::$app->request;
$subscription = $request->getRawBody();
- if(empty($subscription)) {
+ if (empty($subscription)) {
throw new InvalidArgumentException('Missing subscription data');
}
- $decoded = json_decode($subscription);
+ $decoded = json_decode($subscription, false);
$endpoint = $decoded->endpoint;
// check if exists a subscriber with the same endpoint
@@ -68,12 +68,11 @@ public function actionSubscribe()
$message = '';
- if($subscriber) {
+ if ($subscriber) {
$subscriber->subscription = $subscription;
$subscriber->save();
$message = 'user subscription updated';
- }
- else {
+ } else {
$subscriber = new WebPushSubscription();
$subscriber->subscription = $subscription;
$subscriber->endpoint = $endpoint;
@@ -101,16 +100,16 @@ public function actionUnsubscribe()
$request = Yii::$app->request;
$subscription = $request->getRawBody();
- if(empty($subscription)) {
+ if (empty($subscription)) {
throw new InvalidArgumentException('Missing subscription data');
}
- $decoded = json_decode($subscription);
+ $decoded = json_decode($subscription, false);
$endpoint = $decoded->endpoint;
$subscriber = WebPushSubscription::findOne(['endpoint' => $endpoint]);
- if($subscriber) {
+ if ($subscriber) {
$subscriber->delete();
}
@@ -126,14 +125,15 @@ public function actionUnsubscribe()
/**
* @return \yii\console\Response|Response
*/
- public function actionServiceWorker() {
+ public function actionServiceWorker()
+ {
$app_root = Yii::getAlias("@app");
$filepath = '/service-worker.js';
$module = Yii::$app->getModule('notifications');
- if(!empty($module->channels['web']['config']['serviceWorkerFilepath'])) {
+ if (!empty($module->channels['web']['config']['serviceWorkerFilepath'])) {
$filepath = $module->channels['web']['config']['serviceWorkerFilepath'];
}
diff --git a/messages/de/modules/notifications.php b/messages/de/modules/notifications.php
index ba1b831..5dad8e9 100644
--- a/messages/de/modules/notifications.php
+++ b/messages/de/modules/notifications.php
@@ -22,9 +22,10 @@
'Delete all' => 'Alle löschen',
'Mark all as read' => 'Alle als gelesen markieren',
'Mark as read' => 'Als gelesen markieren',
+ 'Mark as unread' => 'Als nicht gelesen markieren',
'Notifications' => 'Benachrichtigungen',
- 'Read' => 'Lesen',
- 'There are no notifications to show' => 'Es sind keine Benachrichtigungen zu zeigen',
+ 'Read' => 'Gelesen',
+ 'There are no notifications to show' => 'Es sind keine neuen Benachrichtigungen vorhanden',
'View all' => 'Alle ansehen',
'a moment' => 'ein Moment',
'ago' => 'vor',
diff --git a/messages/es/modules/notifications.php b/messages/es/modules/notifications.php
index fe44888..d38fce1 100644
--- a/messages/es/modules/notifications.php
+++ b/messages/es/modules/notifications.php
@@ -22,6 +22,7 @@
'Delete all' => 'Eliminar todas',
'Mark all as read' => 'Marcar todas como leidas',
'Mark as read' => 'Marcar como leida',
+ 'Mark as unread' => 'Marcar como no leida',
'Notifications' => 'Notificaciones',
'Read' => 'Leer',
'There are no notifications to show' => 'No hay notificaciones para mostrar',
diff --git a/messages/it/modules/notifications.php b/messages/it/modules/notifications.php
index dcc757e..c9f65df 100644
--- a/messages/it/modules/notifications.php
+++ b/messages/it/modules/notifications.php
@@ -22,6 +22,7 @@
'Delete all' => 'Cancella tutto',
'Mark all as read' => 'Segna tutto come letto',
'Mark as read' => 'Segna come letto',
+ 'Mark as unread' => 'Segna come non letto',
'Notifications' => 'Notifiche',
'Read' => 'Leggi',
'There are no notifications to show' => 'Nessuna notifica da mostrare',
diff --git a/messages/pt/modules/notifications.php b/messages/pt/modules/notifications.php
index f83b44c..bad42d5 100644
--- a/messages/pt/modules/notifications.php
+++ b/messages/pt/modules/notifications.php
@@ -22,6 +22,7 @@
'Delete all' => 'Apagar tudo',
'Mark all as read' => 'Marcar tudo como lido',
'Mark as read' => 'Marcar como lido',
+ 'Mark as unread' => 'Marcar como não lido',
'Notifications' => 'Notificações',
'Read' => 'Lido',
'There are no notifications to show' => 'Não há notificações para mostrar',
diff --git a/views/default/index.php b/views/default/index.php
index d150c57..167ce7e 100644
--- a/views/default/index.php
+++ b/views/default/index.php
@@ -1,8 +1,15 @@
title = Yii::t('modules/notifications', 'Notifications');
?>
@@ -14,23 +21,23 @@
-
+
= LinkPager::widget(['pagination' => $pagination]); ?>
diff --git a/widgets/Notifications.php b/widgets/Notifications.php
index 7ddd265..aced79a 100644
--- a/widgets/Notifications.php
+++ b/widgets/Notifications.php
@@ -120,9 +120,12 @@ public function registerAssets()
'url' => Url::to(['/notifications/default/list']),
'countUrl' => Url::to(['/notifications/default/count']),
'readUrl' => Url::to(['/notifications/default/read']),
+ 'unreadUrl' => Url::to(['/notifications/default/unread']),
'readAllUrl' => Url::to(['/notifications/default/read-all']),
'xhrTimeout' => Html::encode($this->xhrTimeout),
'pollInterval' => Html::encode($this->pollInterval),
+ 'readLabel' => Yii::t('modules/notifications', 'Mark as unread'),
+ 'markAsReadLabel' => Yii::t('modules/notifications', 'Mark as read'),
], $this->clientOptions);
$js = 'Notifications(' . Json::encode($this->clientOptions) . ');';