From 4341fcab0977f3189431430585aba02cd0ac308d Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Wed, 18 Jun 2014 12:34:19 +0200 Subject: [PATCH] Implemented notification stacking --- wcfsetup/install/files/js/WCF.User.js | 8 +- .../files/lib/data/user/UserProfile.class.php | 11 + .../UserNotificationAction.class.php | 133 ++++++---- .../UserNotificationEditor.class.php | 16 ++ .../UserNotificationHandler.class.php | 248 ++++++++++++------ .../AbstractUserNotificationEvent.class.php | 40 ++- .../event/IUserNotificationEvent.class.php | 20 +- ...owFollowingUserNotificationEvent.class.php | 31 ++- ...UserFollowUserNotificationObject.class.php | 7 + wcfsetup/install/files/style/dropdown.less | 10 + wcfsetup/install/files/style/icon.less | 6 + wcfsetup/install/lang/de.xml | 4 +- wcfsetup/install/lang/en.xml | 2 + wcfsetup/setup/db/install.sql | 29 +- 14 files changed, 397 insertions(+), 168 deletions(-) diff --git a/wcfsetup/install/files/js/WCF.User.js b/wcfsetup/install/files/js/WCF.User.js index d23e4046e9..7baa9d22e6 100644 --- a/wcfsetup/install/files/js/WCF.User.js +++ b/wcfsetup/install/files/js/WCF.User.js @@ -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 '' + oldHTML + ''; - }).children('a').data('notificationID', $container.data('notificationID')).click($.proxy(this._clickLink, this)); + if (!$container.data('isGrouped')) { + $container.find('p').html(function(index, oldHTML) { + return '' + oldHTML + ''; + }).children('a').data('notificationID', $container.data('notificationID')).click($.proxy(this._clickLink, this)); + } }, this)); this._badge = $('.jsNotificationsBadge:eq(0)'); diff --git a/wcfsetup/install/files/lib/data/user/UserProfile.class.php b/wcfsetup/install/files/lib/data/user/UserProfile.class.php index f720cecd68..eb9a15d7cc 100644 --- a/wcfsetup/install/files/lib/data/user/UserProfile.class.php +++ b/wcfsetup/install/files/lib/data/user/UserProfile.class.php @@ -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 ''.$this->username.''; + } } diff --git a/wcfsetup/install/files/lib/data/user/notification/UserNotificationAction.class.php b/wcfsetup/install/files/lib/data/user/notification/UserNotificationAction.class.php index 9288bb13b4..87ed2ce2fc 100644 --- a/wcfsetup/install/files/lib/data/user/notification/UserNotificationAction.class.php +++ b/wcfsetup/install/files/lib/data/user/notification/UserNotificationAction.class.php @@ -1,10 +1,12 @@ 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'], diff --git a/wcfsetup/install/files/lib/data/user/notification/UserNotificationEditor.class.php b/wcfsetup/install/files/lib/data/user/notification/UserNotificationEditor.class.php index 7ac2d0011f..52d53d509b 100644 --- a/wcfsetup/install/files/lib/data/user/notification/UserNotificationEditor.class.php +++ b/wcfsetup/install/files/lib/data/user/notification/UserNotificationEditor.class.php @@ -1,6 +1,7 @@ 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)); + } } diff --git a/wcfsetup/install/files/lib/system/user/notification/UserNotificationHandler.class.php b/wcfsetup/install/files/lib/system/user/notification/UserNotificationHandler.class.php index 867db78d8e..41c7b7956e 100644 --- a/wcfsetup/install/files/lib/system/user/notification/UserNotificationHandler.class.php +++ b/wcfsetup/install/files/lib/system/user/notification/UserNotificationHandler.class.php @@ -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'); diff --git a/wcfsetup/install/files/lib/system/user/notification/event/AbstractUserNotificationEvent.class.php b/wcfsetup/install/files/lib/system/user/notification/event/AbstractUserNotificationEvent.class.php index b33bfbac10..5c3b7296da 100644 --- a/wcfsetup/install/files/lib/system/user/notification/event/AbstractUserNotificationEvent.class.php +++ b/wcfsetup/install/files/lib/system/user/notification/event/AbstractUserNotificationEvent.class.php @@ -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; + } } diff --git a/wcfsetup/install/files/lib/system/user/notification/event/IUserNotificationEvent.class.php b/wcfsetup/install/files/lib/system/user/notification/event/IUserNotificationEvent.class.php index 48659f9070..bf8f23da7f 100644 --- a/wcfsetup/install/files/lib/system/user/notification/event/IUserNotificationEvent.class.php +++ b/wcfsetup/install/files/lib/system/user/notification/event/IUserNotificationEvent.class.php @@ -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(); } diff --git a/wcfsetup/install/files/lib/system/user/notification/event/UserFollowFollowingUserNotificationEvent.class.php b/wcfsetup/install/files/lib/system/user/notification/event/UserFollowFollowingUserNotificationEvent.class.php index 89e6fd1808..e98073a3f6 100644 --- a/wcfsetup/install/files/lib/system/user/notification/event/UserFollowFollowingUserNotificationEvent.class.php +++ b/wcfsetup/install/files/lib/system/user/notification/event/UserFollowFollowingUserNotificationEvent.class.php @@ -1,8 +1,6 @@ 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() */ diff --git a/wcfsetup/install/files/lib/system/user/notification/object/UserFollowUserNotificationObject.class.php b/wcfsetup/install/files/lib/system/user/notification/object/UserFollowUserNotificationObject.class.php index aec2f5fc2e..c5672a57bc 100644 --- a/wcfsetup/install/files/lib/system/user/notification/object/UserFollowUserNotificationObject.class.php +++ b/wcfsetup/install/files/lib/system/user/notification/object/UserFollowUserNotificationObject.class.php @@ -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; + } } diff --git a/wcfsetup/install/files/style/dropdown.less b/wcfsetup/install/files/style/dropdown.less index 36f60bef9d..8a4a369c60 100644 --- a/wcfsetup/install/files/style/dropdown.less +++ b/wcfsetup/install/files/style/dropdown.less @@ -243,6 +243,16 @@ } &.notificationItem { + &.groupedNotificationItem > a { + > div:first-child { + padding: 4px 2px 2px; + } + + > div + div { + margin-left: 32px; + } + } + > a { white-space: normal; } diff --git a/wcfsetup/install/files/style/icon.less b/wcfsetup/install/files/style/icon.less index e175ea9292..c86374fe94 100644 --- a/wcfsetup/install/files/style/icon.less +++ b/wcfsetup/install/files/style/icon.less @@ -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; diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 5dc43d9913..5a49c5d95a 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -2782,7 +2782,9 @@ Sollten Sie sich nicht auf der Website: {@PAGE_TITLE|language} angemeldet haben, username}“ folgt Ihnen.]]> + 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.]]> + username} folgt Ihnen.]]> - + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 16d57c15b3..4ba4f6c050 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -2637,8 +2637,10 @@ You can safely ignore this email if you did not register with the website: {@PAG username}” follows you.]]> + 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.]]> username} follows you.]]> +