* Notifications: fixed deleting and grouping of popup notifications by app-name and -id and show full total independent of max. 100 shown entries

This commit is contained in:
ralf 2024-04-16 18:53:54 +02:00
parent 4d31227b43
commit 4b683b38ea
10 changed files with 133 additions and 62 deletions

View File

@ -2287,7 +2287,7 @@ class Db
if ($this->Debug) echo "<p>sql='$sql'</p>";
if ($line === false && $file === false) // call by union, to return the sql rather then run the query
if ($line === false && $file === false) // call by union, to return the sql rather than run the query
{
return $sql;
}

View File

@ -596,7 +596,7 @@ abstract class Tracking
if (!$this->notify_current_user && $this->user) // do we have a current user and should we notify the current user about his own changes
{
//error_log("do_notificaton() adding user=$this->user to email_sent, to not notify him");
//error_log("do_notification() adding user=$this->user to email_sent, to not notify him");
$email_sent[] = $GLOBALS['egw']->accounts->id2name($this->user,'account_email');
}
$skip_notify = $this->get_config('skip_notify',$data,$old);
@ -722,8 +722,8 @@ abstract class Tracking
*
* Called by track() or externally for sending async notifications
*
* Method changes $GLOBALS['egw_info']['user'], so everything called by it, eg. get_(subject|body|links|attachements),
* must NOT store something from user enviroment! By the end of the method, everything get changed back.
* Method changes $GLOBALS['egw_info']['user'], so everything called by it, e.g. get_(subject|body|links|attachments),
* must NOT store something from user environment! By the end of the method, everything get changed back.
*
* @param array $data current entry
* @param array $old = null old/last state of the entry or null for a new entry
@ -807,7 +807,7 @@ abstract class Tracking
$attachments = $this->get_attachments($data,$old,$receiver);
}
// restore user enviroment BEFORE calling notification class or returning
// restore user environment BEFORE calling notification class or returning
$GLOBALS['egw_info']['user'] = $save_user;
// need to call preferences constructor and read_repository, to set user timezone again
$GLOBALS['egw']->preferences->__construct($GLOBALS['egw_info']['user']['account_id']);
@ -843,12 +843,12 @@ abstract class Tracking
$notification->set_reply_to($reply_to);
$notification->set_subject($subject);
$notification->set_links(array($link));
$notification->set_popupdata($link?$link['app']:null, $link);
$notification->set_popupdata($link['app']??null, $link, $link['id']??null);
if ($attachments && is_array($attachments))
{
$notification->set_attachments($attachments);
}
// run immediatly during async service, as sending mail with Horde fails, if PHP is already in shutdown
// run immediately during async service, as sending mail with Horde fails, if PHP is already in shutdown
// (json requests take care of that by calling Egw::__desctruct() explicit before it's regular triggered)
$run = isset($GLOBALS['egw_info']['flags']['async-service']) ? 'call_user_func_array' : Api\Egw::class.'::on_shutdown';
$run(static function($notification, $sender, $receiver, $subject)
@ -1088,7 +1088,7 @@ abstract class Tracking
* @param array $data
* @param array $old
* @param boolean $integrate_link to have links embedded inside the body
* @param int|string $receiver nummeric account_id or email address
* @param int|string $receiver numeric account_id or email address
* @return string
*/
public function get_body($html_email,$data,$old,$integrate_link = true,$receiver=null)

View File

@ -1236,9 +1236,9 @@ class calendar_boupdate extends calendar_bo
// popup notifiactions: set subject, different message (without separator) and (always) links
$notification->set_popupsubject($subject);
if ($method =='REQUEST')
if ($method == 'REQUEST')
{
// Add ACCEPT|REHECT|TENTATIVE actions
// Add ACCEPT|REJECT|TENTATIVE actions
$notification->set_popupdata('calendar', array(
'event_id' => $event['id'],
'user_id' => $userid,

View File

@ -593,7 +593,7 @@ class notifications {
}
break; // stop running through chain
}
// backend sucseeded
// backend succeeded
$user_notified = true;
if($action == 'stop' || $action == 'fail') { break; } // stop running through chain
}
@ -827,13 +827,15 @@ class notifications {
* Set popup data
*
* @param string $_appname
* @param array $_data
* @param ?array $_data
*
* @return boolean
*/
public function set_popupdata($_appname, $_data)
public function set_popupdata(string $_appname, ?array $_data, ?int $_id=null)
{
$this->popup_data = array(
'appname' => $_appname,
'id' => $_id ?: $_data['id'] ?? null,
'data' => $_data
);

View File

@ -129,13 +129,14 @@ class notifications_ajax
}
/**
* Remove given notification id(s) from the table
* Remove given notification id(s) and app_ids from the table
*
* @param int[]|array[] $notifymessages one or multiple notify_id(s) or objects incl. id attribute
* @param array[] $app_ids app-name int[] pairs
*/
public function delete_message(array $notifymessages)
public function delete_message(array $notifymessages, array $app_ids=[])
{
$this->update($notifymessages, null); // null = delete
$this->update($notifymessages, null, $app_ids ?? []); // null = delete
// if we delete all messages (we either delete one or all!), we return the next chunk of messages directly
if (count($notifymessages) > 1)
@ -165,11 +166,12 @@ class notifications_ajax
*
* @param array $notifymessages
* @param string|null $status use null to delete
* @param array[] $app_ids app-name int[] pairs
* @return array
*/
protected function update(array $notifymessages, $status='SEEN')
protected function update(array $notifymessages, $status='SEEN', array $app_ids=[])
{
$notify_ids = $app_ids = [];
$notify_ids = [];
foreach ($notifymessages as $data)
{
if (is_array($data) && !empty($data['id']))
@ -187,14 +189,13 @@ class notifications_ajax
}
$cut_off = $this->db->quote(Api\DateTime::to(self::CUT_OFF_DATE, Api\DateTime::DATABASE));
try {
// MariaDB code using JSON_EXTRACT()
foreach($app_ids as $app => $ids)
{
$where = [
'account_id' => $this->recipient->account_id,
'notify_type' => self::_type,
"JSON_EXTRACT(notify_data, '$.appname') = ".$this->db->quote($app),
"JSON_EXTRACT(notify_data, '$.data.id') IN (".implode(',', array_map([$this->db, 'quote'], array_unique($ids))).')',
'notify_app' => $app,
'notify_app_id' => array_unique($ids),
'notify_created > '.$cut_off,
];
if (isset($status))
@ -207,21 +208,8 @@ class notifications_ajax
}
}
}
// other DBs
catch (Api\Db\Exception $e) {
foreach($this->db->select(self::_notification_table, 'notify_id,notify_data', [
'account_id' => $this->recipient->account_id,
'notify_type' => self::_type,
'notify_created > '.$cut_off,
"notify_data <> '[]'", // does not return NULL or '[]' rows
], __LINE__, __FILE__, false,'', self::_appname) as $row)
{
if (($data = json_decode($row['notify_data'], true)) &&
isset($data['data']['id']) && in_array($data['data']['id'], $app_ids[$data['appname']] ?? []))
{
$notify_ids[] = $row['notify_id'];
}
}
// ignore, if DB is not yet updated with notify_app(_id) columns
}
$where = [
'notify_id' => array_unique($notify_ids),

View File

@ -18,7 +18,7 @@ use EGroupware\Api;
* @abstract egwpopup is a two stage notification. In the first stage
* notification is written into self::_notification_table.
* In the second stage a request from the client reads
* out the table to look if there is a notificaton for this
* out the table to look if there is a notification for this
* client. The second stage is done in class.notifications_ajax.inc.php
*/
class notifications_popup implements notifications_iface
@ -128,6 +128,8 @@ class notifications_popup implements notifications_iface
'notify_type' => self::_type,
'notify_data' => $_data && is_array($_data) ? json_encode($_data) : NULL,
'notify_created' => new Api\DateTime(),
'notify_app' => $_data['appname'],
'notify_app_id' => $_data['id'],
), false,__LINE__,__FILE__,self::_appname);
if ($result === false) throw new Exception("Can't save notification into SQL table");
$push = new Api\Json\Push($this->recipient->account_id);
@ -154,10 +156,20 @@ class notifications_popup implements notifications_iface
$result = [];
if (($total = $db->select(self::_notification_table, 'COUNT(*)', [
'account_id' => $_account_id,
'notify_type' => self::_type,
'notify_created > '.($cut_off=$db->quote(Api\DateTime::to(notifications_ajax::CUT_OFF_DATE, Api\DateTime::DATABASE))),
], __LINE__, __FILE__, false, '', self::_appname)->fetchColumn()))
'account_id' => $_account_id,
'notify_type' => self::_type,
'notify_created > '.($cut_off=$db->quote(Api\DateTime::to(notifications_ajax::CUT_OFF_DATE, Api\DateTime::DATABASE))),
'notify_app_id IS NULL',
], __LINE__, __FILE__, false, '', self::_appname)->fetchColumn()+
$db->select(
'('.$db->select(self::_notification_table, 'notify_app,notify_app_id', [
'account_id' => $_account_id,
'notify_type' => self::_type,
'notify_created > '.($cut_off=$db->quote(Api\DateTime::to(notifications_ajax::CUT_OFF_DATE, Api\DateTime::DATABASE))),
'notify_app_id IS NOT NULL',
], false, false, false, 'GROUP BY notify_app,notify_app_id', self::_appname).') AS app_ids',
'COUNT(*)', false, __LINE__, __FILE__, false, '', self::_appname)->fetchColumn()
))
{
$n = 0;
$chunk_size = 150;
@ -188,26 +200,22 @@ class notifications_popup implements notifications_iface
'current' => new Api\DateTime('now'),
'actions' => is_array($actions) ? $actions : NULL,
'extra_data' => $data['data'] ?? [],
'app' => $notification['notify_app'] ?? $data['data']['app'] ?? null,
'app_id' => $notification['notify_app_id'] ?? $data['data']['id'] ?? null,
];
// aggregate by app:id reporting only the newest entry
if (!empty($data['extra_data']['id']))
if (!empty($data['app_id']))
{
if (!isset($result[$id = $data['extra_data']['app'] . ':' . $data['extra_data']['id']]))
if (!isset($result[$id = $data['app'] . ':' . $data['app_id']]))
{
$result[$id] = $data;
}
else
{
$total--;
/* in case we want to show all
$result['id']['others'][] = $data;
*/
}
}
else
{
$result[] = $data;
}
if (count($result) >= min($num_rows, $total)) break;
}
$n += $chunk_size;
}

View File

@ -6,7 +6,6 @@
* @subpackage ajaxpoup
* @link http://www.egroupware.org
* @author Cornelius Weiss <nelius@cwtech.de>, Christian Binder <christian@jaytraxx.de>, Ralf Becker <rb@egroupware.org>
* @version $Id$
*/
'use strict';
@ -570,7 +569,17 @@
notifications.prototype.delete_all = function () {
if (!notifymessages || Object.entries(notifymessages).length == 0) return false;
egw.request("notifications.notifications_ajax.delete_message", [Object.keys(notifymessages)]);
const app_ids = {};
for(const id in notifymessages)
{
const notification = notifymessages[id];
if (notification.data?.id && notification.data.app)
{
if (typeof app_ids[notification.data.app] === "undefined") app_ids[notification.data.app] = [];
app_ids[notification.data.app].push(notification.data.id);
}
}
egw.request("notifications.notifications_ajax.delete_message", [Object.keys(notifymessages), app_ids]);
notifymessages = {};
_currentRawData = []; // otherwise response from delete_message/get_notifications might not get parsed
this.total = 0;
@ -585,11 +594,14 @@
*/
notifications.prototype.button_delete = function(_node, _event) {
_event.stopPropagation();
var egwpopup_message = _node[0];
var id = egwpopup_message[0].id.replace(/egwpopup_message_/ig,'');
egw.request("notifications.notifications_ajax.delete_message", [[id]]);
var nextNode = egwpopup_message.next();
var keepLoadingPrompt = false;
const egwpopup_message = _node[0];
const id = egwpopup_message[0].id.replace(/egwpopup_message_/ig,'');
const notification = notifymessages[id];
const app_ids = {};
if (notification.data?.id) app_ids[notification.data.app] = [notification.data.id];
egw.request("notifications.notifications_ajax.delete_message", [[id], app_ids]);
const nextNode = egwpopup_message.next();
let keepLoadingPrompt = false;
delete (notifymessages[id]);
this.total -= 1;
this.counterUpdate();
@ -935,7 +947,8 @@
// toggle notifications bar
jQuery('.button_right_toggle', '#egwpopup').click(function(){window.app.notifications.toggle();});
$egwpopup_fw.click(function(){window.app.notifications.toggle();});
jQuery(".egwpopup_deleteall", '#egwpopup').click(function(){
jQuery(".egwpopup_deleteall", '#egwpopup').click(function(_ev){
_ev.stopPropagation();
et2_dialog.show_dialog( function(_button){
if (_button == 2) window.app.notifications.delete_all();
},

View File

@ -14,7 +14,7 @@ if (!defined('NOTIFICATION_APP'))
}
$setup_info[NOTIFICATION_APP]['name'] = NOTIFICATION_APP;
$setup_info[NOTIFICATION_APP]['version'] = '23.1';
$setup_info[NOTIFICATION_APP]['version'] = '23.1.001';
$setup_info[NOTIFICATION_APP]['app_order'] = 1;
$setup_info[NOTIFICATION_APP]['tables'] = array('egw_notificationpopup');
$setup_info[NOTIFICATION_APP]['enable'] = 2;
@ -39,4 +39,4 @@ $setup_info[NOTIFICATION_APP]['hooks']['config'] = 'notifications.notifications.
$setup_info[NOTIFICATION_APP]['depends'][] = array(
'appname' => 'api',
'versions' => Array('23.1')
);
);

View File

@ -18,11 +18,13 @@ $phpgw_baseline = array(
'notify_created' => array('type' => 'timestamp','meta' => 'timestamp','default' => 'current_timestamp','comment' => 'creation time of notification'),
'notify_type' => array('type' => 'ascii','precision' => '32','comment' => 'notification type'),
'notify_status' => array('type' => 'varchar','precision' => '32','comment' => 'notification status'),
'notify_data' => array('type' => 'varchar','precision' => '4096','comment' => 'notification data')
'notify_data' => array('type' => 'varchar','precision' => '4096','comment' => 'notification data'),
'notify_app' => array('type' => 'ascii','precision' => '16','comment' => 'appname'),
'notify_app_id' => array('type' => 'ascii','precision' => '64','comment' => 'application id')
),
'pk' => array('notify_id'),
'fk' => array(),
'ix' => array('account_id','notify_created'),
'ix' => array('notify_created',array('account_id','notify_type'),array('notify_app','notify_app_id')),
'uc' => array()
)
);
);

View File

@ -9,6 +9,8 @@
* @subpackage setup
*/
use EGroupware\Api;
function notifications_upgrade0_5()
{
$GLOBALS['egw_setup']->oProc->AlterColumn('egw_notificationpopup','account_id',array(
@ -201,4 +203,60 @@ function notifications_upgrade21_1()
));
return $GLOBALS['setup_info']['notifications']['currentver'] = '23.1';
}
/**
* Add explicit columns for app-name and -id
*
* @return string
*/
function notifications_upgrade23_1()
{
$GLOBALS['egw_setup']->oProc->AddColumn('egw_notificationpopup','notify_app',array(
'type' => 'ascii',
'precision' => '16',
'comment' => 'appname'
));
$GLOBALS['egw_setup']->oProc->AddColumn('egw_notificationpopup','notify_app_id',array(
'type' => 'ascii',
'precision' => '64',
'comment' => 'application id'
));
/** @var Api\Db $db */
$db = $GLOBALS['egw_setup']->db;
try {
$db->update('egw_notificationpopup',array(
"notify_app=JSON_VALUE(notify_data,'$.data.app')",
"notify_app_id=JSON_VALUE(notify_data,'$.data.id')",
), 'notify_data IS NOT NULL', __LINE__, __FILE__, 'notifications');
}
catch (Api\Db\Exception\InvalidSql $e) {
$start = 0;
$chunk_size = 1000;
do
{
foreach($rs=$db->select('egw_notificationpopup','notify_id,notify_data','notify_data IS NOT NULL',
__LINE__, __FILE__, $start, '', 'notifications', $chunk_size) as $row)
{
$data = json_decode($row['notify_data']);
if ($data && !empty($data['data']['id']))
{
$db->update('egw_notficationpopup', [
'notify_app' => $data['data']['app'],
'notify_id' => $data['data']['id'],
], ['notify_id' => $row['notify_id']], __LINE__, __FILE__, 'notifications');
}
++$start;
}
}
while ($start && !($start % $chunk_size));
}
$GLOBALS['egw_setup']->oProc->CreateIndex('egw_notificationpopup', array('notify_app','notify_app_id'), false);
$GLOBALS['egw_setup']->oProc->CreateIndex('egw_notificationpopup', array('account_id','notify_type'), false);
$GLOBALS['egw_setup']->oProc->DropIndex('egw_notificationpopup', array('account_id'));
return $GLOBALS['setup_info']['notifications']['currentver'] = '23.1.001';
}