Implemented notification stacking
authorAlexander Ebert <ebert@woltlab.com>
Wed, 18 Jun 2014 10:34:19 +0000 (12:34 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 18 Jun 2014 10:34:19 +0000 (12:34 +0200)
14 files changed:
wcfsetup/install/files/js/WCF.User.js
wcfsetup/install/files/lib/data/user/UserProfile.class.php
wcfsetup/install/files/lib/data/user/notification/UserNotificationAction.class.php
wcfsetup/install/files/lib/data/user/notification/UserNotificationEditor.class.php
wcfsetup/install/files/lib/system/user/notification/UserNotificationHandler.class.php
wcfsetup/install/files/lib/system/user/notification/event/AbstractUserNotificationEvent.class.php
wcfsetup/install/files/lib/system/user/notification/event/IUserNotificationEvent.class.php
wcfsetup/install/files/lib/system/user/notification/event/UserFollowFollowingUserNotificationEvent.class.php
wcfsetup/install/files/lib/system/user/notification/object/UserFollowUserNotificationObject.class.php
wcfsetup/install/files/style/dropdown.less
wcfsetup/install/files/style/icon.less
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index d23e4046e94c028f57c3b301634be65b5557f7fd..7baa9d22e635ec8ddda0a141309cb1187ae94f10 100644 (file)
@@ -1394,9 +1394,11 @@ WCF.Notification.List = Class.extend({
                        this._items[$container.data('notificationID')] = $container;
                        
                        $container.find('.jsMarkAsConfirmed').data('notificationID', $container.data('notificationID')).click($.proxy(this._click, this));
-                       $container.find('p').html(function(index, oldHTML) {
-                               return '<a>' + oldHTML + '</a>';
-                       }).children('a').data('notificationID', $container.data('notificationID')).click($.proxy(this._clickLink, this));
+                       if (!$container.data('isGrouped')) {
+                               $container.find('p').html(function(index, oldHTML) {
+                                       return '<a>' + oldHTML + '</a>';
+                               }).children('a').data('notificationID', $container.data('notificationID')).click($.proxy(this._clickLink, this));
+                       }
                }, this));
                
                this._badge = $('.jsNotificationsBadge:eq(0)');
index f720cecd68d9160b734b425efe866865c00a0607..eb9a15d7cc40e6ed753cf0a45e6d4cc36e9f2725 100644 (file)
@@ -802,4 +802,15 @@ class UserProfile extends DatabaseObjectDecorator implements IBreadcrumbProvider
                
                return $username;
        }
+       
+       /**
+        * Returns a HTML anchor link pointing to the decorated user.
+        * 
+        * @return      string
+        */
+       public function getAnchorTag() {
+               $link = LinkHandler::getInstance()->getLink('User', array('object' => $this->getDecoratedObject()));
+               
+               return '<a href="'.$link.'" class="userLink" data-user-id="'.$this->userID.'">'.$this->username.'</a>';
+       }
 }
index 9288bb13b4fee49340a30d572d8c8bc216126695..87ed2ce2fc56854b6bfd58c394942cfb591e9c66 100644 (file)
@@ -1,10 +1,12 @@
 <?php
 namespace wcf\data\user\notification;
-use wcf\data\user\notification\UserNotificationEditor;
 use wcf\data\AbstractDatabaseObjectAction;
 use wcf\system\user\notification\UserNotificationHandler;
 use wcf\system\user\storage\UserStorageHandler;
 use wcf\system\WCF;
+use wcf\system\exception\UserInputException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
 
 /**
  * Executes user notification-related actions.
@@ -18,37 +20,79 @@ use wcf\system\WCF;
  */
 class UserNotificationAction extends AbstractDatabaseObjectAction {
        /**
-        * Adds notification recipients.
+        * notification object
+        * @var \wcf\data\user\notification\UserNotification
         */
-       public function addRecipients() {
-               $sql = "INSERT IGNORE INTO      wcf".WCF_N."_user_notification_to_user
-                                               (notificationID, userID, mailNotified)
-                       VALUES                  (?, ?, ?)";
-               $statement = WCF::getDB()->prepareStatement($sql);
-               
-               foreach ($this->objects as $notification) {
-                       foreach ($this->parameters['recipients'] as $recipient) {
-                               $statement->execute(array($notification->notificationID, $recipient->userID, ($recipient->mailNotificationType == 'daily' ? 0 : 1)));
-                       }
-               }
-       }
+       public $notification = null;
        
        /**
         * @see \wcf\data\AbstractDatabaseObjectAction::create()
         */
        public function create() {
-               // create notification
                $notification = parent::create();
                
-               // save recpients
-               if (!empty($this->parameters['recipients'])) {
-                       $action = new UserNotificationAction(array($notification), 'addRecipients', array(
-                               'recipients' => $this->parameters['recipients']         
+               $sql = "INSERT INTO     wcf".WCF_N."_user_notification_to_user
+                                       (notificationID, userID)
+                       VALUES          (?, ?)";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute(array(
+                       $notification->notificationID,
+                       $notification->userID
+               ));
+               
+               return $notification;
+       }
+       
+       public function createDefault() {
+               die("createDefault");
+       }
+       
+       public function createStackable() {
+               // get existing notifications
+               $notificationList = new UserNotificationList();
+               $notificationList->getConditionBuilder()->add("eventID = ?", array($this->parameters['data']['eventID']));
+               $notificationList->getConditionBuilder()->add("objectID = ?", array($this->parameters['data']['objectID']));
+               $notificationList->getConditionBuilder()->add("userID IN (?)", array(array_keys($this->parameters['recipients'])));
+               $notificationList->getConditionBuilder()->add("confirmed = ?", array(0));
+               $notificationList->readObjects();
+               $existingNotifications = array();
+               foreach ($notificationList as $notification) {
+                       $existingNotifications[$notification->userID] = $notification;
+               }
+               
+               $notifications = array();
+               foreach ($this->parameters['recipients'] as $recipient) {
+                       $notification = (isset($existingNotifications[$recipient->userID]) ? $existingNotifications[$recipient->userID] : null);
+                       $isNew = ($notification === null);
+                       
+                       if ($notification === null) {
+                               $this->parameters['data']['userID'] = $recipient->userID;
+                               $notification = $this->create();
+                       }
+                       
+                       $notifications[$recipient->userID] = array(
+                               'isNew' => $isNew,
+                               'object' => $notification
+                       );
+               }
+               
+               // insert author
+               $sql = "INSERT INTO     wcf".WCF_N."_user_notification_author
+                                       (notificationID, authorID, time)
+                       VALUES          (?, ?, ?)";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               
+               WCF::getDB()->beginTransaction();
+               foreach ($notifications as $notificationData) {
+                       $statement->execute(array(
+                               $notificationData['object']->notificationID,
+                               ($this->parameters['authorID'] ?: null),
+                               TIME_NOW
                        ));
-                       $action->executeAction();
                }
+               WCF::getDB()->commitTransaction();
                
-               return $notification;
+               return $notifications;
        }
        
        /**
@@ -69,14 +113,9 @@ class UserNotificationAction extends AbstractDatabaseObjectAction {
                        'notifications' => $notifications
                ));
                
-               $totalCount = UserNotificationHandler::getInstance()->getNotificationCount();
-               if (count($notifications) < $totalCount) {
-                       UserStorageHandler::getInstance()->reset(array(WCF::getUser()->userID), 'userNotificationCount');
-               }
-               
                return array(
                        'template' => WCF::getTPL()->fetch('notificationListOustanding'),
-                       'totalCount' => $totalCount
+                       'totalCount' => UserNotificationHandler::getInstance()->getNotificationCount(true)
                );
        }
        
@@ -85,21 +124,13 @@ class UserNotificationAction extends AbstractDatabaseObjectAction {
         */
        public function validateMarkAsConfirmed() {
                $this->readInteger('notificationID');
+               $this->notification = new UserNotification($this->parameters['notificationID']);
                
-               $sql = "SELECT  COUNT(*) AS count
-                       FROM    wcf".WCF_N."_user_notification_to_user
-                       WHERE   notificationID = ?
-                               AND userID = ?";
-               $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute(array(
-                       $this->parameters['notificationID'],
-                       WCF::getUser()->userID
-               ));
-               $row = $statement->fetchArray();
-               
-               // pretend it was marked as confirmed
-               if (!$row['count']) {
-                       $this->parameters['alreadyConfirmed'] = true;
+               if (!$this->notification->notificationID) {
+                       throw new UserInputException('notificationID');
+               }
+               else if ($this->notification->userID != WCF::getUser()->userID) {
+                       throw new PermissionDeniedException();
                }
        }
        
@@ -109,21 +140,11 @@ class UserNotificationAction extends AbstractDatabaseObjectAction {
         * @return      array
         */
        public function markAsConfirmed() {
-               if (!isset($this->parameters['alreadyConfirmed'])) {
-                       $sql = "UPDATE  wcf".WCF_N."_user_notification_to_user
-                               SET     confirmed = ?
-                               WHERE   notificationID = ?
-                                       AND userID = ?";
-                       $statement = WCF::getDB()->prepareStatement($sql);
-                       $statement->execute(array(
-                               1,
-                               $this->parameters['notificationID'],
-                               WCF::getUser()->userID
-                       ));
-                       
-                       // reset notification count
-                       UserStorageHandler::getInstance()->reset(array(WCF::getUser()->userID), 'userNotificationCount');
-               }
+               $notificationEditor = new UserNotificationEditor($this->notification);
+               $notificationEditor->markAsConfirmed();
+               
+               // reset notification count
+               UserStorageHandler::getInstance()->reset(array(WCF::getUser()->userID), 'userNotificationCount');
                
                return array(
                        'notificationID' => $this->parameters['notificationID'],
index 7ac2d0011f32443f4da9db0c6092b0d03bb54c5d..52d53d509b5faa9032cc711720733e00ac1109ed 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace wcf\data\user\notification;
 use wcf\data\DatabaseObjectEditor;
+use wcf\system\WCF;
 
 /**
  * Provides functions to edit user notifications.
@@ -17,4 +18,19 @@ class UserNotificationEditor extends DatabaseObjectEditor {
         * @see \wcf\data\DatabaseObjectDecorator::$baseClass
         */
        protected static $baseClass = 'wcf\data\user\notification\UserNotification';
+       
+       /**
+        * Marks this notification as confirmed.
+        */
+       public function markAsConfirmed() {
+               $this->update(array(
+                       'confirmed' => 1
+               ));
+               
+               // delete notification_to_user assignment (mimic legacy notification system)
+               $sql = "DELETE FROM     wcf".WCF_N."_user_notification_to_user
+                       WHERE           notificationID = ?";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute(array($this->notificationID));
+       }
 }
index 867db78d8e6622791d00f13e01ccf74288e8a3a6..41c7b7956e817bb1859e7067081184d67103d4f0 100644 (file)
@@ -105,23 +105,50 @@ class UserNotificationHandler extends SingletonFactory {
                // set object data
                $event->setObject(new UserNotification(null, array()), $notificationObject, $userProfile, $additionalData);
                
-               // find existing events
-               $userIDs = array();
-               $conditionBuilder = new PreparedStatementConditionBuilder();
-               $conditionBuilder->add('notification_to_user.notificationID = notification.notificationID');
-               $conditionBuilder->add('notification_to_user.userID IN (?)', array($recipientIDs));
-               $conditionBuilder->add('notification.eventHash = ?', array($event->getEventHash()));
-               $sql = "SELECT  notification_to_user.userID
-                       FROM    wcf".WCF_N."_user_notification notification,
-                               wcf".WCF_N."_user_notification_to_user notification_to_user
-                       ".$conditionBuilder;
+               // find existing notifications
+               $conditions = new PreparedStatementConditionBuilder();
+               $conditions->add("userID IN (?)", array($recipientIDs));
+               $conditions->add("eventID = ?", array($event->eventID));
+               $conditions->add("objectID = ?", array($notificationObject->getObjectID()));
+               $conditions->add("confirmed = ?", array(0));
+               
+               $sql = "SELECT  notificationID, userID
+                       FROM    wcf".WCF_N."_user_notification
+                       ".$conditions;
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute($conditionBuilder->getParameters());
-               while ($row = $statement->fetchArray()) $userIDs[] = $row['userID'];
+               $statement->execute($conditions->getParameters());
+               $notifications = array();
+               while ($row = $statement->fetchArray()) {
+                       $notifications[$row['userID']] = $row['notificationID'];
+               }
                
                // skip recipients with outstanding notifications
-               if (!empty($userIDs)) {
-                       $recipientIDs = array_diff($recipientIDs, $userIDs);
+               if (!empty($notifications)) {
+                       // filter by author
+                       if ($notificationObject->getAuthorID()) {
+                               $conditions = new PreparedStatementConditionBuilder();
+                               $conditions->add("notificationID IN (?)", array(array_values($notifications)));
+                               $conditions->add("authorID = ?", array($notificationObject->getAuthorID()));
+                               
+                               $sql = "SELECT  notificationID
+                                       FROM    wcf".WCF_N."_user_notification_author
+                                       ".$conditions;
+                               $statement = WCF::getDB()->prepareStatement($sql);
+                               $statement->execute($conditions->getParameters());
+                               $notificationIDs = array();
+                               while ($row = $statement->fetchArray()) {
+                                       $notificationIDs[] = $row['notificationID'];
+                               }
+                               
+                               foreach ($notifications as $userID => $notificationID) {
+                                       // do not skip recipients with a similar notification but authored by somebody else
+                                       if (!in_array($notificationID, $notificationIDs)) {
+                                               unset($notifications[$userID]);
+                                       }
+                               }
+                       }
+                       
+                       $recipientIDs = array_diff($recipientIDs, array_keys($notifications));
                        if (empty($recipientIDs)) return;
                }
                
@@ -130,53 +157,72 @@ class UserNotificationHandler extends SingletonFactory {
                $recipientList->getConditionBuilder()->add('event_to_user.eventID = ?', array($event->eventID));
                $recipientList->getConditionBuilder()->add('event_to_user.userID IN (?)', array($recipientIDs));
                $recipientList->readObjects();
-               if (count($recipientList)) {
-                       // find existing notification
-                       $notification = UserNotification::getNotification($objectTypeObject->packageID, $event->eventID, $notificationObject->getObjectID());
-                       if ($notification !== null) {
-                               // only update recipients
-                               $action = new UserNotificationAction(array($notification), 'addRecipients', array(
-                                       'recipients' => $recipientList->getObjects()
-                               ));
-                               $action->executeAction();
+               $recipients = $recipientList->getObjects();
+               if (!empty($recipients)) {
+                       $data = array(
+                               'authorID' => ($event->getAuthorID() ?: null),
+                               'data' => array(
+                                       'eventID' => $event->eventID,
+                                       'authorID' => ($event->getAuthorID() ?: null),
+                                       'objectID' => $notificationObject->getObjectID(),
+                                       'time' => TIME_NOW,
+                                       'additionalData' => serialize($additionalData)
+                               ),
+                               'recipients' => $recipients
+                       );
+                       
+                       if ($event->isStackable()) {
+                               $data['notifications'] = $notifications;
+                               
+                               $action = new UserNotificationAction(array(), 'createStackable', $data);
                        }
                        else {
-                               // create new notification
-                               $action = new UserNotificationAction(array(), 'create', array(
-                                       'data' => array(
-                                               'packageID' => $objectTypeObject->packageID,
-                                               'eventID' => $event->eventID,
-                                               'objectID' => $notificationObject->getObjectID(),
-                                               'authorID' => ($event->getAuthorID() ?: null),
-                                               'time' => TIME_NOW,
-                                               'eventHash' => $event->getEventHash(),
-                                               'additionalData' => serialize($additionalData)
-                                       ),
-                                       'recipients' => $recipientList->getObjects()
-                               ));
-                               $result = $action->executeAction();
-                               $notification = $result['returnValues'];
+                               $action = new UserNotificationAction(array(), 'createDefault', $data);
                        }
                        
-                       // sends notifications
-                       foreach ($recipientList->getObjects() as $recipient) {
+                       $result = $action->executeAction();
+                       $notifications = $result['returnValues'];
+                       
+                       /*
+                       // create new notification
+                       $action = new UserNotificationAction(array(), 'create', array(
+                               'authorID' => ($event->getAuthorID() ?: null),
+                               'data' => array(
+                                       'eventID' => $event->eventID,
+                                       'authorID' => ($event->getAuthorID() ?: null),
+                                       'objectID' => $notificationObject->getObjectID(),
+                                       'time' => TIME_NOW,
+                                       'additionalData' => serialize($additionalData)
+                               ),
+                               'recipients' => $recipients
+                       ));
+                       $result = $action->executeAction();
+                       $notifications = $result['returnValues'];
+                       */
+                       
+                       // TODO: move -> DBOAction?
+                       // send notifications
+                       foreach ($recipients as $recipient) {
                                if ($recipient->mailNotificationType == 'instant') {
-                                       $this->sendInstantMailNotification($notification, $recipient, $event);
+                                       if (isset($notifications[$recipient->userID]) && $notifications[$recipient->userID]['isNew']) {
+                                               $this->sendInstantMailNotification($notifications[$recipient->userID]['object'], $recipient, $event);
+                                       }
                                }
                        }
                        
                        // reset notification count
-                       UserStorageHandler::getInstance()->reset($recipientList->getObjectIDs(), 'userNotificationCount');
+                       UserStorageHandler::getInstance()->reset(array_keys($recipients), 'userNotificationCount');
                }
        }
        
        /**
         * Returns the number of outstanding notifications for the active user.
         * 
+        * @param       boolean         $skipCache
         * @return      integer
         */
-       public function getNotificationCount() {
-               if ($this->notificationCount === null) {
+       public function getNotificationCount($skipCache = false) {
+               if ($this->notificationCount === null || $skipCache) {
                        $this->notificationCount = 0;
                        
                        if (WCF::getUser()->userID) {
@@ -187,18 +233,17 @@ class UserNotificationHandler extends SingletonFactory {
                                $data = UserStorageHandler::getInstance()->getStorage(array(WCF::getUser()->userID), 'userNotificationCount');
                                
                                // cache does not exist or is outdated
-                               if ($data[WCF::getUser()->userID] === null) {
-                                       $conditionBuilder = new PreparedStatementConditionBuilder();
-                                       $conditionBuilder->add('notification.notificationID = notification_to_user.notificationID');
-                                       $conditionBuilder->add('notification_to_user.userID = ?', array(WCF::getUser()->userID));
-                                       $conditionBuilder->add('notification_to_user.confirmed = ?', array(0));
-                                       
+                               if ($data[WCF::getUser()->userID] === null || $skipCache) {
                                        $sql = "SELECT  COUNT(*) AS count
-                                               FROM    wcf".WCF_N."_user_notification_to_user notification_to_user,
-                                                       wcf".WCF_N."_user_notification notification
-                                               ".$conditionBuilder->__toString();
+                                               FROM    wcf".WCF_N."_user_notification
+                                               WHERE   userID = ?
+                                                       AND confirmed = ?";
                                        $statement = WCF::getDB()->prepareStatement($sql);
-                                       $statement->execute($conditionBuilder->getParameters());
+                                       $statement->execute(array(
+                                               WCF::getUser()->userID,
+                                               0
+                                       ));
+                                       
                                        $row = $statement->fetchArray();
                                        $this->notificationCount = $row['count'];
                                        
@@ -221,7 +266,7 @@ class UserNotificationHandler extends SingletonFactory {
         */
        public function countAllNotifications() {
                $sql = "SELECT  COUNT(*) AS count
-                       FROM    wcf".WCF_N."_user_notification_to_user
+                       FROM    wcf".WCF_N."_user_notification
                        WHERE   userID = ?";
                $statement = WCF::getDB()->prepareStatement($sql);
                $statement->execute(array(WCF::getUser()->userID));
@@ -241,22 +286,20 @@ class UserNotificationHandler extends SingletonFactory {
        public function getNotifications($limit = 5, $offset = 0, $showConfirmedNotifications = false) {
                // build enormous query
                $conditions = new PreparedStatementConditionBuilder();
-               $conditions->add("notification_to_user.userID = ?", array(WCF::getUser()->userID));
-               if (!$showConfirmedNotifications) $conditions->add("notification_to_user.confirmed = ?", array(0));
-               $conditions->add("notification.notificationID = notification_to_user.notificationID");
-               
-               $sql = "SELECT          notification_to_user.notificationID, notification_event.eventID,
-                                       object_type.objectType, notification.objectID,
-                                       notification.additionalData, notification.authorID,
-                                       notification.time".($showConfirmedNotifications ? ", notification_to_user.confirmed" : "")."
-                       FROM            wcf".WCF_N."_user_notification_to_user notification_to_user,
-                                       wcf".WCF_N."_user_notification notification
+               $conditions->add("notification.userID = ?", array(WCF::getUser()->userID));
+               if (!$showConfirmedNotifications) $conditions->add("notification.confirmed = ?", array(0));
+               
+               $sql = "SELECT          notification.notificationID, notification_event.eventID, notification.authorID,
+                                       notification.moreAuthors, object_type.objectType, notification.objectID,
+                                       notification.additionalData,
+                                       notification.time".($showConfirmedNotifications ? ", notification.confirmed" : "")."
+                       FROM            wcf".WCF_N."_user_notification notification
                        LEFT JOIN       wcf".WCF_N."_user_notification_event notification_event
                        ON              (notification_event.eventID = notification.eventID)
                        LEFT JOIN       wcf".WCF_N."_object_type object_type
                        ON              (object_type.objectTypeID = notification_event.objectTypeID)
                        ".$conditions."
-                       ORDER BY        notification.time DESC";
+                       ORDER BY        ".($showConfirmedNotifications ? "notification.confirmed ASC, " : "")."notification.time DESC";
                $statement = WCF::getDB()->prepareStatement($sql, $limit, $offset);
                $statement->execute($conditions->getParameters());
                
@@ -276,7 +319,6 @@ class UserNotificationHandler extends SingletonFactory {
                        $objectTypes[$row['objectType']]['objectIDs'][] = $row['objectID'];
                        $eventIDs[] = $row['eventID'];
                        $notificationIDs[] = $row['notificationID'];
-                       $authorIDs[] = $row['authorID'];
                }
                
                // return an empty set if no notifications exist
@@ -287,6 +329,29 @@ class UserNotificationHandler extends SingletonFactory {
                        );
                }
                
+               // load authors
+               $conditions = new PreparedStatementConditionBuilder();
+               $conditions->add("notificationID IN (?)", array($notificationIDs));
+               $sql = "SELECT          notificationID, authorID
+                       FROM            wcf".WCF_N."_user_notification_author
+                       ".$conditions."
+                       ORDER BY        time ASC";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute($conditions->getParameters());
+               $authorIDs = $authorToNotification = array();
+               while ($row = $statement->fetchArray()) {
+                       if (!$row['authorID']) {
+                               continue;
+                       }
+                       
+                       if (!isset($authorToNotification[$row['notificationID']])) {
+                               $authorToNotification[$row['notificationID']] = array();
+                       }
+                       
+                       $authorIDs[] = $row['authorID'];
+                       $authorToNotification[$row['notificationID']][] = $row['authorID'];
+               }
+               
                // load authors
                $authors = UserProfile::getUserProfiles($authorIDs);
                $unknownAuthor = new UserProfile(new User(null, array('userID' => null, 'username' => WCF::getLanguage()->get('wcf.user.guest'))));
@@ -313,15 +378,29 @@ class UserNotificationHandler extends SingletonFactory {
                foreach ($events as $event) {
                        $className = $eventObjects[$event['eventID']]->className;
                        $class = new $className($eventObjects[$event['eventID']]);
+                       $notificationID = $event['notificationID'];
                        
                        $class->setObject(
-                               $notificationObjects[$event['notificationID']],
+                               $notificationObjects[$notificationID],
                                $objectTypes[$event['objectType']]['objects'][$event['objectID']],
                                (isset($authors[$event['authorID']]) ? $authors[$event['authorID']] : $unknownAuthor),
                                unserialize($event['additionalData'])
                        );
                        
+                       if (isset($authorToNotification[$notificationID])) {
+                               $eventAuthors = array();
+                               foreach ($authorToNotification[$notificationID] as $userID) {
+                                       if (isset($authors[$userID])) {
+                                               $eventAuthors[$userID] = $authors[$userID];
+                                       }
+                               }
+                               if (!empty($eventAuthors)) {
+                                       $class->setAuthors($eventAuthors);
+                               }
+                       }
+                       
                        $data = array(
+                               'authors' => count($class->getAuthors()),
                                'event' => $class,
                                'notificationID' => $event['notificationID'],
                                'time' => $event['time']
@@ -516,23 +595,30 @@ class UserNotificationHandler extends SingletonFactory {
                $objectTypeObject = $this->availableObjectTypes[$objectType];
                $event = $this->availableEvents[$objectType][$eventName];
                
-               // delete notifications
-               $sql = "UPDATE  wcf".WCF_N."_user_notification_to_user
+               // mark as confirmed
+               $conditions = new PreparedStatementConditionBuilder();
+               $conditions->add("eventID = ?", array($event->eventID));
+               if (!empty($recipientIDs)) $conditions->add("userID IN (?)", array($recipientIDs));
+               if (!empty($objectIDs)) $conditions->add("objectID IN (?)", array($objectIDs));
+               
+               $sql = "UPDATE  wcf".WCF_N."_user_notification
                        SET     confirmed = ?
-                       WHERE   notificationID IN (
-                                       SELECT  notificationID
-                                       FROM    wcf".WCF_N."_user_notification
-                                       WHERE   packageID = ?
-                                               AND eventID = ?
-                                               ".(!empty($objectIDs) ? "AND objectID IN (?".(count($objectIDs) > 1 ? str_repeat(',?', count($objectIDs) - 1) : '').")" : '')." 
-                               )
-                               ".(!empty($recipientIDs) ? ("AND userID IN (?".(count($recipientIDs) > 1 ? str_repeat(',?', count($recipientIDs) - 1) : '').")") : '');
-               $parameters = array(1, $objectTypeObject->packageID, $event->eventID);
-               if (!empty($objectIDs)) $parameters = array_merge($parameters, $objectIDs);
-               if (!empty($recipientIDs)) $parameters = array_merge($parameters, $recipientIDs);
+                       ".$conditions;
                $statement = WCF::getDB()->prepareStatement($sql);
+               $parameters = $conditions->getParameters();
+               array_unshift($parameters, 1);
                $statement->execute($parameters);
                
+               // delete notification_to_user assignments (mimic legacy notification system)
+               $sql = "DELETE FROM     wcf".WCF_N."_user_notification_to_user
+                       WHERE           notificationID NOT IN (
+                                               SELECT  notificationID
+                                               FROM    wcf".WCF_N."_user_notification
+                                               WHERE   confirmed = ?
+                                       )";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute(array(0));
+               
                // reset storage
                if (!empty($recipientIDs)) {
                        UserStorageHandler::getInstance()->reset($recipientIDs, 'userNotificationCount');
index b33bfbac1069e11e7b0a48bdcaab2978acf9877d..5c3b7296da804cd3b41fc403ba0c19e41eea44ec 100644 (file)
@@ -30,6 +30,18 @@ abstract class AbstractUserNotificationEvent extends DatabaseObjectDecorator imp
         */
        protected $author = null;
        
+       /**
+        * list of authors for stacked notifications
+        * @var array<\wcf\data\user\UserProfile>
+        */
+       protected $authors = array();
+       
+       /**
+        * notification stacking support
+        * @var boolean
+        */
+       protected $isStackable = false;
+       
        /**
         * user notification
         * @var \wcf\data\user\notification\UserNotification
@@ -54,6 +66,13 @@ abstract class AbstractUserNotificationEvent extends DatabaseObjectDecorator imp
         */
        protected $language = null;
        
+       /**
+        * @see \wcf\system\user\notification\event\IUserNotificationEvent::setAuthors()
+        */
+       public function setAuthors(array $authors) {
+               $this->authors = $authors;
+       }
+       
        /**
         * @see \wcf\system\user\notification\event\IUserNotificationEvent::setObject()
         */
@@ -78,6 +97,13 @@ abstract class AbstractUserNotificationEvent extends DatabaseObjectDecorator imp
                return $this->author;
        }
        
+       /**
+        * @see \wcf\system\user\notification\event\IUserNotificationEvent::getAuthors()
+        */
+       public function getAuthors() {
+               return $this->authors;
+       }
+       
        /**
         * @see \wcf\system\user\notification\event\IUserNotificationEvent::isVisible()
         */
@@ -123,13 +149,6 @@ abstract class AbstractUserNotificationEvent extends DatabaseObjectDecorator imp
                return $this->getMessage();
        }
        
-       /**
-        * @see \wcf\system\user\notification\event\IUserNotificationEvent::getEventHash()
-        */
-       public function getEventHash() {
-               return StringUtil::getHash($this->packageID . '-'. $this->eventID . '-' . $this->userNotificationObject->getObjectID());
-       }
-       
        /**
         * @see \wcf\system\user\notification\event\IUserNotificationEvent::setLanguage()
         */
@@ -146,4 +165,11 @@ abstract class AbstractUserNotificationEvent extends DatabaseObjectDecorator imp
                if ($this->language !== null) return $this->language;
                return WCF::getLanguage();
        }
+       
+       /**
+        * @see \wcf\system\user\notification\event\IUserNotificationEvent::isStackable()
+        */
+       public function isStackable() {
+               return $this->isStackable;
+       }
 }
index 48659f90706617a535cb238339b5ace8532c85cf..bf8f23da7feb3bc8e08e04a5243fa9dca9d685dc 100644 (file)
@@ -67,6 +67,13 @@ interface IUserNotificationEvent extends IDatabaseObjectProcessor {
         */
        public function getAuthor();
        
+       /**
+        * Returns a list of authors for stacked notifications sorted by time.
+        * 
+        * @return      array<\wcf\data\user\UserProfile>
+        */
+       public function getAuthors();
+       
        /**
         * Returns true if this notification event is visible for the active user.
         * 
@@ -75,11 +82,11 @@ interface IUserNotificationEvent extends IDatabaseObjectProcessor {
        public function isVisible();
        
        /**
-        * Returns a unique identifier of the event.
+        * Sets a list of authors for stacked notifications.
         * 
-        * @return      string
+        * @param       array<\wcf\data\user\UserProfile>       $authors
         */
-       public function getEventHash();
+       public function setAuthors(array $authors);
        
        /**
         * Sets the object for the event.
@@ -97,4 +104,11 @@ interface IUserNotificationEvent extends IDatabaseObjectProcessor {
         * @param       \wcf\data\language\Language     $language
         */
        public function setLanguage(Language $language);
+       
+       /**
+        * Returns true if this notification event supports stacking.
+        * 
+        * @return      boolean
+        */
+       public function isStackable();
 }
index 89e6fd18080103fc4ec16a14e90ea2ec609adf83..e98073a3f64aec08ee3338ed74904b60029d3526 100644 (file)
@@ -1,8 +1,6 @@
 <?php
 namespace wcf\system\user\notification\event;
 use wcf\system\request\LinkHandler;
-use wcf\system\user\notification\event\AbstractUserNotificationEvent;
-use wcf\util\StringUtil;
 
 /**
  * Notification event for followers.
@@ -15,10 +13,20 @@ use wcf\util\StringUtil;
  * @category   Community Framework
  */
 class UserFollowFollowingUserNotificationEvent extends AbstractUserNotificationEvent {
+       /**
+        * @see \wcf\system\user\notification\event\AbstractUserNotificationEvent::$isStackable
+        */
+       protected $isStackable = true;
+       
        /**
         * @see \wcf\system\user\notification\event\IUserNotificationEvent::getTitle()
         */
        public function getTitle() {
+               $count = count($this->getAuthors());
+               if ($count > 1) {
+                       return $this->getLanguage()->getDynamicVariable('wcf.user.notification.follow.title.stacked', array('count' => $count));
+               }
+               
                return $this->getLanguage()->get('wcf.user.notification.follow.title');
        }
        
@@ -26,6 +34,18 @@ class UserFollowFollowingUserNotificationEvent extends AbstractUserNotificationE
         * @see \wcf\system\user\notification\event\IUserNotificationEvent::getMessage()
         */
        public function getMessage() {
+               $authors = array_values($this->getAuthors());
+               $count = count($authors);
+               
+               if ($count > 1) {
+                       return $this->getLanguage()->getDynamicVariable('wcf.user.notification.follow.message.stacked', array(
+                               'author' => $this->author,
+                               'authors' => $authors,
+                               'count' => $count,
+                               'others' => max($count - 1, 0)
+                       ));
+               }
+               
                return $this->getLanguage()->getDynamicVariable('wcf.user.notification.follow.message', array('author' => $this->author));
        }
        
@@ -36,13 +56,6 @@ class UserFollowFollowingUserNotificationEvent extends AbstractUserNotificationE
                return $this->getLanguage()->getDynamicVariable('wcf.user.notification.follow.mail', array('author' => $this->author));
        }
        
-       /**
-        * @see \wcf\system\user\notification\event\IUserNotificationEvent::getEventHash()
-        */
-       public function getEventHash() {
-               return StringUtil::getHash($this->packageID . '-'. $this->eventID . '-' . $this->author->userID);
-       }
-       
        /**
         * @see \wcf\system\user\notification\event\IUserNotificationEvent::getLink()
         */
index aec2f5fc2e8500a3863a81f1cab1e196738dd220..c5672a57bc1107722446947f3d409abb3d1bee96 100644 (file)
@@ -39,4 +39,11 @@ class UserFollowUserNotificationObject extends DatabaseObjectDecorator implement
        public function getAuthorID() {
                return $this->userID;
        }
+       
+       /**
+        * @see \wcf\data\DatabaseObjectDecorator::getObjectID()
+        */
+       public function getObjectID() {
+               return $this->followUserID;
+       }
 }
index 36f60bef9d7e945a04df908219b69943aa08193f..8a4a369c60e5abf738ad4f1a33b8066b17f9de5f 100644 (file)
                }
                
                &.notificationItem {
+                       &.groupedNotificationItem > a {
+                               > div:first-child {
+                                       padding: 4px 2px 2px;
+                               }
+                               
+                               > div + div {
+                                       margin-left: 32px;
+                               }
+                       }
+                       
                        > a {
                                white-space: normal;
                        }
index e175ea9292747f6297eb2364ba3bc9266cd0bfbf..c86374fe94e31391e4b82ed9b0fd170438cd5de3 100644 (file)
@@ -87,6 +87,12 @@ a > span.fa:not(.pointer) {
        width: 16px;
 }
 
+.icon24 {
+       font-size: 18px;
+       height: 24px;
+       width: 24px;
+}
+
 .icon32 {
        font-size: 28px;
        height: 32px;
index 5dc43d9913c24482f7884ad9f04a00ac57dd0d04..5a49c5d95a3941354ca4520d305a82bf6cc18a14 100644 (file)
@@ -2782,7 +2782,9 @@ Sollten Sie sich nicht auf der Website: {@PAGE_TITLE|language} angemeldet haben,
                <item name="wcf.user.notification.button.confirmed"><![CDATA[OK]]></item>
                <item name="wcf.user.notification.count"><![CDATA[if (data.returnValues.count == 0) { "Keine Benachrichtigungen" } else if (data.returnValues.count == 1) { "Eine Benachrichtigung" } else { data.returnValues.count + " Benachrichtigungen" }]]></item>
                <item name="wcf.user.notification.follow.message"><![CDATA[„{$author->username}“ folgt Ihnen.]]></item>
+               <item name="wcf.user.notification.follow.message.stacked"><![CDATA[{if $count < 4}{@$authors[0]->getAnchorTag()}{if $count == 2} und {else}, {/if}{@$authors[1]->getAnchorTag()}{if $count == 3} und {@$authors[2]->getAnchorTag()}{/if}{else}{@$authors[0]->getAnchorTag()} und {#$count} weiteren{/if} folgen Ihnen.]]></item>
                <item name="wcf.user.notification.follow.title"><![CDATA[Neuer Follower]]></item>
+               <item name="wcf.user.notification.follow.title.stacked"><![CDATA[{#$count} neue Follower]]></item>
                <item name="wcf.user.notification.follow.mail"><![CDATA[{@$author->username} folgt Ihnen.]]></item>
                <item name="wcf.user.notification.mail.disabled"><![CDATA[Die E-Mail-Benachrichtigung wurde erfolgreich abgeschaltet.]]></item>
                <item name="wcf.user.notification.mail.footer"><![CDATA[Diese E-Mail ist eine automatische Benachrichtigung. BITTE ANTWORTEN SIE NICHT AUF DIESE E-MAIL.
@@ -2805,7 +2807,7 @@ Möchten Sie diese E-Mail-Benachrichtigung in Zukunft nicht mehr erhalten, könn
                <item name="wcf.user.notification.mailNotificationType.none"><![CDATA[Keine E-Mail-Benachrichtigung]]></item>
                <item name="wcf.user.notification.mailNotificationType.instant"><![CDATA[Sofortige E-Mail-Benachrichtigung]]></item>
                <item name="wcf.user.notification.mailNotificationType.daily"><![CDATA[Tägliche E-Mail-Benachrichtigung]]></item>
-               <item name="wcf.user.notification.markAllAsConfirmed"><![CDATA[Alle Benachrichtigungen verwerfen]]></item>
+               <item name="wcf.user.notification.markAllAsConfirmed"><![CDATA[Alle als gelesen markieren]]></item>
                <item name="wcf.user.notification.markAllAsConfirmed.confirmMessage"><![CDATA[Wollen Sie wirklich alle Benachrichtigungen als gelesen markieren?]]></item>
                <item name="wcf.user.notification.markAsConfirmed"><![CDATA[Alls gelesen markieren]]></item>
                <item name="wcf.user.notification.noMoreNotifications"><![CDATA[Keine neuen Benachrichtigungen]]></item>
index 16d57c15b326a53e753b92e50cff794e6350cae7..4ba4f6c0506deb6e65574153aaa38ba890cc6253 100644 (file)
@@ -2637,8 +2637,10 @@ You can safely ignore this email if you did not register with the website: {@PAG
                <item name="wcf.user.notification.button.confirmed"><![CDATA[OK]]></item>
                <item name="wcf.user.notification.count"><![CDATA[if (data.returnValues.count == 0) { "No Notifications" } else if (data.returnValues.count == 1) { "1 Notification" } else { data.returnValues.count + " Notifications" }]]></item>
                <item name="wcf.user.notification.follow.message"><![CDATA[“{$author->username}” follows you.]]></item>
+               <item name="wcf.user.notification.follow.message.stacked"><![CDATA[{if $count < 4}{@$authors[0]->getAnchorTag()}{if $count == 2} and {else}, {/if}{@$authors[1]->getAnchorTag()}{if $count == 3} and {@$authors[2]->getAnchorTag()}{/if}{else}{@$authors[0]->getAnchorTag()} and {#$count} others{/if} follow you.]]></item>
                <item name="wcf.user.notification.follow.mail"><![CDATA[{@$author->username} follows you.]]></item>
                <item name="wcf.user.notification.follow.title"><![CDATA[New Follower]]></item>
+               <item name="wcf.user.notification.follow.title.stacked"><![CDATA[{#$count} new followers]]></item>
                <item name="wcf.user.notification.mail.disabled"><![CDATA[Email notification has been disabled.]]></item>
                <item name="wcf.user.notification.mail.footer"><![CDATA[This is an automatic notification, PLEASE DO NOT REPLY TO THIS EMAIL!
                
index 05569c1a724f57914c2524d431c15db368fcf8f5..6d764f2326d218528ee6e67a3796d6b768960734 100644 (file)
@@ -1201,24 +1201,34 @@ CREATE TABLE wcf1_user_menu_item (
 DROP TABLE IF EXISTS wcf1_user_notification;
 CREATE TABLE wcf1_user_notification (
        notificationID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
-       packageID INT(10) NOT NULL,
        eventID INT(10) NOT NULL,
        objectID INT(10) NOT NULL DEFAULT 0,
-       eventHash VARCHAR(40) NOT NULL DEFAULT '',
-       authorID INT(10),
+       authorID INT(10) NULL,
+       moreAuthors INT(10) NOT NULL DEFAULT 0,
+       userID INT(10) NOT NULL,
        time INT(10) NOT NULL DEFAULT 0,
+       mailNotified TINYINT(1) NOT NULL DEFAULT 0,
+       confirmed TINYINT(1) NOT NULL DEFAULT 0,
        additionalData TEXT,
-       KEY (eventHash),
-       UNIQUE KEY (packageID, eventID, objectID)
+       KEY (userID, eventID, objectID, confirmed)
+);
+
+-- notification authors (stacking)
+DROP TABLE IF EXISTS wcf1_user_notification_author;
+CREATE TABLE wcf1_user_notification_author (
+       notificationID INT(10) NOT NULL,
+       authorID INT(10) NOT NULL,
+       time INT(10) NOT NULL DEFAULT 0,
+       canceled TINYINT(1) NOT NULL DEFAULT 0,
+       UNIQUE KEY (notificationID, authorID)
 );
 
 -- notification recipients
+-- DEPRECATED
 DROP TABLE IF EXISTS wcf1_user_notification_to_user;
 CREATE TABLE wcf1_user_notification_to_user (
        notificationID INT(10) NOT NULL,
        userID INT(10) NOT NULL,
-       mailNotified TINYINT(1) NOT NULL DEFAULT 0,
-       confirmed TINYINT(1) NOT NULL DEFAULT 0,
        UNIQUE KEY notificationID (notificationID, userID)
 );
 
@@ -1544,9 +1554,12 @@ ALTER TABLE wcf1_user_ignore ADD FOREIGN KEY (ignoreUserID) REFERENCES wcf1_user
 
 ALTER TABLE wcf1_user_menu_item ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
 
-ALTER TABLE wcf1_user_notification ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
 ALTER TABLE wcf1_user_notification ADD FOREIGN KEY (eventID) REFERENCES wcf1_user_notification_event (eventID) ON DELETE CASCADE;
 ALTER TABLE wcf1_user_notification ADD FOREIGN KEY (authorID) REFERENCES wcf1_user (userID) ON DELETE SET NULL;
+ALTER TABLE wcf1_user_notification ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;
+
+ALTER TABLE wcf1_user_notification_author ADD FOREIGN KEY (notificationID) REFERENCES wcf1_user_notification (notificationID) ON DELETE CASCADE;
+ALTER TABLE wcf1_user_notification_author ADD FOREIGN KEY (authorID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;
 
 ALTER TABLE wcf1_user_notification_to_user ADD FOREIGN KEY (notificationID) REFERENCES wcf1_user_notification (notificationID) ON DELETE CASCADE;
 ALTER TABLE wcf1_user_notification_to_user ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;