diff --git a/src/configs/Config.php b/src/configs/Config.php index adb96f771..ab7cb262d 100755 --- a/src/configs/Config.php +++ b/src/configs/Config.php @@ -336,6 +336,8 @@ nsdefine('ERROR_INFO', 4); nsconddefine("MAINTENANCE_MODE", false); /** Constant used to indicate lasting an arbitrary number of seconds */ nsdefine('FOREVER', -2); +/** Constant used to indicate most recent occurence of an impression type */ +nsdefine('MOST_RECENT_VIEW', -4); /** Number of seconds in a day*/ nsdefine('ONE_DAY', 86400); /** Number of seconds in a week*/ diff --git a/src/controllers/components/AccountaccessComponent.php b/src/controllers/components/AccountaccessComponent.php index 147c67c43..e7c6ebffc 100644 --- a/src/controllers/components/AccountaccessComponent.php +++ b/src/controllers/components/AccountaccessComponent.php @@ -178,6 +178,9 @@ class AccountaccessComponent extends Component $item = $group_model->getMostRecentGroupPost($group_id); $data['GROUPS'][$i]['NUM_POSTS'] = $group_model->getGroupPostCount($group_id); + $data['GROUPS'][$i]['NEW_POSTS'] = + $group_model->getGroupPostCount($group_id, + $data['GROUPS'][$i]["MOST_RECENT_VIEW"]); $data['GROUPS'][$i]['NUM_THREADS'] = $group_model->getGroupThreadCount($group_id); $data['GROUPS'][$i]['NUM_PAGES'] = diff --git a/src/controllers/components/Component.php b/src/controllers/components/Component.php index d4ca59507..57db3862d 100644 --- a/src/controllers/components/Component.php +++ b/src/controllers/components/Component.php @@ -199,6 +199,26 @@ class Component { $parent = $this->parent; $group_model = $parent->model("group"); + $impression_model = $parent->model("impression"); $data['NUM_GROUPS'] = $group_model->countUserGroups($user_id); + $timestamp = $impression_model->mostRecentGroupView($user_id); + $data['UNREAD_POSTS'] = $group_model->getUserPostCount($user_id, + $timestamp); + $personal_group_id = $group_model->getPersonalGroupId($user_id); + $contact_ids = array_diff($group_model->getGroupUserIds( + $personal_group_id), [$user_id]); + $chat_ids = []; + $unread_count = 0; + foreach ($contact_ids as $contact_id) { + $chat_id = $group_model->getGroupThreadId( + $personal_group_id, $user_id, + $group_model->getMessagesThreadTitle( + [$user_id, $contact_id])); + $chat_stamp = $impression_model->mostRecentThreadView($user_id, + $chat_id); + $unread_count += floor($group_model->getThreadPostCount($chat_id, + $chat_stamp) / 2); + } + $data['UNREAD_MESSAGES'] = $unread_count; } } diff --git a/src/controllers/components/SocialComponent.php b/src/controllers/components/SocialComponent.php index cf6af1dd0..194de64c9 100644 --- a/src/controllers/components/SocialComponent.php +++ b/src/controllers/components/SocialComponent.php @@ -2058,9 +2058,10 @@ class SocialComponent extends Component implements CrawlConstants '}; doUpdate();'; $contact_ids = array_diff($group_model->getGroupUserIds( $user_messages_id), [$user_id]); - $contacts = $this->marshallContactInfo($contact_ids, $contact_filter); + $contacts = $this->marshallContactInfo($user_id, $contact_ids, + $contact_filter); $requests_thread_id = $group_model->getGroupThreadId( - $user_messages_id, null, $this->getMessagesThreadTitle( + $user_messages_id, null, $group_model->getMessagesThreadTitle( [$user_id])); $contact_requests = []; if (!empty($requests_thread_id)) { @@ -2085,12 +2086,15 @@ class SocialComponent extends Component implements CrawlConstants $data['ELEMENT'] = 'usermessages'; $data['MESSAGES'] = []; if (!empty($contact_id)) { - $messages_thread_id = $group_model->getGroupThreadId( - $user_messages_id, $user_id, $this->getMessagesThreadTitle( + $message_thread_id = $group_model->getGroupThreadId( + $user_messages_id, $user_id, + $group_model->getMessagesThreadTitle( [$user_id, $contact_id])); - if ($messages_thread_id) { + if ($message_thread_id) { + $parent->model("impression")->add($user_id, $message_thread_id, + C\THREAD_IMPRESSION); $search_array = [ - ["parent_id", "=", $messages_thread_id, ""], + ["parent_id", "=", $message_thread_id, ""], ["group_id", "=", $user_messages_id, ""], ["pubdate", "", "", "DESC"]]; list(, $data['MESSAGES']) = $this->initializeFeedItems($data, @@ -2103,8 +2107,9 @@ class SocialComponent extends Component implements CrawlConstants return $data; } /** - * Gets an array of contact details including USER_NAME and ICON_URL ofr - * an array of contact ids, subject to a filter. + * Gets an array of contact details including USER_NAME, ICON_URL, + * NUM_UNREAD_MESSAGES for + * an array of contact ids for $user_id, subject to a filter. * @param array $contact_ids id's that contact details are desired of * @param string $contact_filter a substring of user_names to restrict * the returned details to (i.e., only contacts whose user name's match @@ -2113,12 +2118,15 @@ class SocialComponent extends Component implements CrawlConstants * is an array with fields USER_NAME, ICON_URL, ...) for ids with * user names matching the filter */ - private function marshallContactInfo($contact_ids, $contact_filter = "") + private function marshallContactInfo($user_id, $contact_ids, + $contact_filter = "") { $parent = $this->parent; $user_model = $parent->model("user"); $group_model = $parent->model("group"); + $impression_model = $parent->model("impression"); $contacts= []; + $personal_group_id = $group_model->getPersonalGroupId($user_id); foreach ($contact_ids as $contact_id) { $contact_username = $user_model->getUsername( $contact_id); @@ -2127,9 +2135,18 @@ class SocialComponent extends Component implements CrawlConstants continue; } $icon_url = $user_model->getUserIconUrl($contact_id); + $chat_id = $group_model->getGroupThreadId( + $personal_group_id, $user_id, + $group_model->getMessagesThreadTitle( + [$user_id, $contact_id])); + $chat_stamp = $impression_model->mostRecentThreadView($user_id, + $chat_id); $contacts[$contact_id] = [ "USER_NAME" => $contact_username, - "ICON_URL" => $icon_url]; + "ICON_URL" => $icon_url, + "NUM_UNREAD_MESSAGES" => floor($group_model->getThreadPostCount( + $chat_id, $chat_stamp)/2) + ]; } return $contacts; } @@ -2149,17 +2166,17 @@ class SocialComponent extends Component implements CrawlConstants $group_model = $parent->model("group"); $data["REFRESH"] = "feedstatus"; $data['MESSAGES'] = []; - $messages_thread_id = $group_model->getGroupThreadId( + $message_thread_id = $group_model->getGroupThreadId( $data['USER_MESSAGES_ID'], $user_id, - $this->getMessagesThreadTitle( + $group_model->getMessagesThreadTitle( [$user_id, $data["CONTACT_ID"]])); $conversation_time = $parent->clean($_REQUEST['conversation_time'], "int") ?? 0; - if (!$messages_thread_id || !$conversation_time) { + if (!$message_thread_id || !$conversation_time) { return $data; } $search_array = [ - ["parent_id", "=", $messages_thread_id, ""], + ["parent_id", "=", $message_thread_id, ""], ["group_id", "=", $data['USER_MESSAGES_ID'], ""], ["pubdate", ">", $conversation_time, "DESC"]]; list(, $data['MESSAGES']) = $this->initializeFeedItems($data, @@ -2199,7 +2216,7 @@ class SocialComponent extends Component implements CrawlConstants tl('social_component_invalid_contact')); } $group_model->addUserGroup($contact_id, $user_messages_id); - $messages_thread_title = $this->getMessagesThreadTitle( + $messages_thread_title = $group_model->getMessagesThreadTitle( [$user_id, $contact_id]); $_REQUEST["contact_id"] = $contact_id; if ($group_model->isUserIdInContacts($user_id, @@ -2211,7 +2228,7 @@ class SocialComponent extends Component implements CrawlConstants $group_model->addGroupItem( $existing_parent_id, $user_messages_id, $user_id, $messages_thread_title, ""); - $request_thread_title = $this->getMessagesThreadTitle( + $request_thread_title = $group_model->getMessagesThreadTitle( [$user_id]); $request_thread_id = $group_model->getGroupThreadId( $user_messages_id, $contact_id, $request_thread_title); @@ -2224,12 +2241,12 @@ class SocialComponent extends Component implements CrawlConstants $group_model->addGroupItem(0, $user_messages_id, $user_id, $messages_thread_title, ""); - $blocked_thread_title = $this->getMessagesThreadTitle( + $blocked_thread_title = $group_model->getMessagesThreadTitle( [$user_id, "blocked"]); $blocked_thread_id = $group_model->getGroupThreadId( $user_messages_id, $contact_id, $blocked_thread_title); if (!$blocked_thread_id) { - $request_thread_title = $this->getMessagesThreadTitle( + $request_thread_title = $group_model->getMessagesThreadTitle( [$contact_id]); } // thread used to keep tract of contact requests for $contact_id @@ -2275,7 +2292,7 @@ class SocialComponent extends Component implements CrawlConstants return $parent->redirectWithMessage( tl('social_component_invalid_contact')); } - $request_thread_title = $this->getMessagesThreadTitle( + $request_thread_title = $group_model->getMessagesThreadTitle( [$user_id]); $request_thread_id = $group_model->getGroupThreadId( $user_messages_id, $contact_id, $request_thread_title); @@ -2321,13 +2338,13 @@ class SocialComponent extends Component implements CrawlConstants tl('social_component_invalid_contact')); } $contact_messages_id = $group_model->getPersonalGroupId($contact_id); - $request_thread_title = $this->getMessagesThreadTitle( + $request_thread_title = $group_model->getMessagesThreadTitle( [$user_id]); $request_thread_id = $group_model->getGroupThreadId( $user_messages_id, $contact_id, $request_thread_title); if (!empty($request_thread_id)) { $group_model->deleteGroupItem($request_thread_id, $contact_id); - $block_thread_title = $this->getMessagesThreadTitle( + $block_thread_title = $group_model->getMessagesThreadTitle( [$contact_id, "blocked"]); $group_model->addGroupItem(0, $contact_messages_id, $user_id, $block_thread_title, @@ -2353,12 +2370,14 @@ class SocialComponent extends Component implements CrawlConstants $parent = $this->parent; $group_model = $parent->model("group"); $user_messages_id = $group_model->getPersonalGroupId($user_id); - $messages_thread_title = $this->getMessagesThreadTitle( + $messages_thread_title = $group_model->getMessagesThreadTitle( [$user_id, $data['CONTACT_ID']]); $description = $contact_id = $parent->clean($_REQUEST["description"] ?? "", 'string'); $message_thread_id = $group_model->getGroupThreadId($user_messages_id, $user_id, $messages_thread_title); + $parent->model("impression")->add($user_id, $message_thread_id, + C\THREAD_IMPRESSION); if (empty($message_thread_id)) { return $parent->redirectWithMessage( tl("social_component_invalid_message_thread"), @@ -2393,19 +2412,6 @@ class SocialComponent extends Component implements CrawlConstants } return $parent->redirectWithMessage("", ['contact_id']); } - /** - * Messages use the same storage mechanism as group posts, so need a title, - * The title used is always computed as the `-` concatenation of the sorted - * id's of the participants in the message session. This method computes - * such a title string from an array of message participants. - * @param array $thread_follower_ids message chat participants - * @return string appropriate title of chat thread. - */ - private function getMessagesThreadTitle($thread_follower_ids) - { - sort($thread_follower_ids); - return implode("-", $thread_follower_ids); - } /** * Determines a list of posts that might need to reply to a post in * a group @@ -2925,12 +2931,20 @@ class SocialComponent extends Component implements CrawlConstants public function addActivityInfoToGroups(&$data) { $group_model = $this->parent->model("group"); + $impression_model = $this->parent->model("impression"); $num_shown = count($data['GROUPS']); $user_id = $_SESSION['USER_ID'] ?? C\PUBLIC_USER_ID; for ($i = 0; $i < $num_shown; $i++) { $group = $data['GROUPS'][$i]; $group_id = $group['GROUP_ID']; $item = $group_model->getMostRecentGroupPost($group_id); + $most_recent_views = $impression_model->mostRecentGroupViews( + $user_id, [$group_id]); + $group["MOST_RECENT_VIEW"] = + $most_recent_views[$group_id] ?? 0; + $group['NEW_POSTS'] = + $group_model->getGroupPostCount($group_id, + $group["MOST_RECENT_VIEW"]); $group['NUM_POSTS'] = $group_model->getGroupPostCount($group_id); $group['NUM_THREADS'] = $group_model->getGroupThreadCount($group_id); diff --git a/src/css/search.css b/src/css/search.css index 47d4e2cf1..62ec87709 100755 --- a/src/css/search.css +++ b/src/css/search.css @@ -4137,6 +4137,24 @@ td.instruct { left:125px; } +.unread-messages +{ + background-color: darkgray; + border-radius: 6px; + border;: solid 1px black; + color: white; + display:inline-block; + padding:3px; + margin:18px 15px 18px 15px; +} +.html-ltr .unread-messages +{ + float: right; +} +.html-rtl .unread-messages +{ + float: left; +} .message-filter { position: relative; diff --git a/src/library/WikiParser.php b/src/library/WikiParser.php index d9224bf8b..fd0fdd69b 100644 --- a/src/library/WikiParser.php +++ b/src/library/WikiParser.php @@ -191,6 +191,8 @@ class WikiParser implements CrawlConstants ['/\r/', ""], ]; $braces_substitutions = [ + ["/{{timestamp}}/si", + "<input type='hidden' name='username' value='" .time(). "' />"], ["/{{username}}/si", "<input type='hidden' name='username' value='[{username}]' />"], ["/{{image-captcha\|(.+?)}}/si", "<div class='csv-captcha'>". diff --git a/src/models/GroupModel.php b/src/models/GroupModel.php index 0e000068c..ad2e949a9 100644 --- a/src/models/GroupModel.php +++ b/src/models/GroupModel.php @@ -1308,17 +1308,79 @@ class GroupModel extends Model implements MediaConstants $row = $db->fetchArray($result); return $row['NUM'] ?? 0; } + /** + * Returns the number of posts to groups that a user belongs to since + * a timestamp + * + * @param int $user_id id of the user to get posts for + * @param int $timestamp only post with value pubdate greater than + * this will be counted + * @return int number of posts + */ + public function getUserPostCount($user_id, $timestamp = 0) + { + $db = $this->db; + $personal_group_id = $this->getPersonalGroupId($user_id); + $subselect = "(SELECT GROUP_ID " . + "FROM USER_GROUP WHERE USER_ID = ? AND STATUS = ". + C\ACTIVE_STATUS . ")"; + $sql = "SELECT COUNT(DISTINCT GI.ID) AS NUM FROM GROUP_ITEM GI ". + "WHERE GI.GROUP_ID IN $subselect AND GI.TITLE NOT LIKE ". + "'%$user_id%' AND GI.PUBDATE > ?"; + $result = $db->execute($sql, [$user_id, $timestamp]); + if (!$result) { + return 0; + } + $row = $db->fetchArray($result); + echo "yo"; + var_dump($row['NUM']); + return $row['NUM'] ?? 0; + } /** * Returns the number of posts to a group * @param int $group_id id of the group to get post count for + * @param int $timestamp only post with value pubdate greater or + * EDIT_DATE than this will be counted * @return int number of posts */ - public function getGroupPostCount($group_id) + public function getGroupPostCount($group_id, $timestamp = 0) { $db = $this->db; $sql = "SELECT COUNT(DISTINCT GI.ID) AS NUM FROM GROUP_ITEM GI - WHERE GI.GROUP_ID = ?"; - $result = $db->execute($sql, [$group_id]); + WHERE GI.GROUP_ID = ? AND GI.PUBDATE > ?"; + $result = $db->execute($sql, [$group_id, $timestamp]); + if (!$result) { + return 0; + } + $row = $db->fetchArray($result); + return $row['NUM'] ?? 0; + } + /** + * Messages use the same storage mechanism as group posts, so need a title, + * The title used is always computed as the `-` concatenation of the sorted + * id's of the participants in the message session. This method computes + * such a title string from an array of message participants. + * @param array $thread_follower_ids message chat participants + * @return string appropriate title of chat thread. + */ + public function getMessagesThreadTitle($thread_follower_ids) + { + sort($thread_follower_ids); + return implode("-", $thread_follower_ids); + } + /** + * Returns the number of posts to a thread + * @param int $thread_id id of the thread to get post count for + * @param int $timestamp only post with value pubdate greater or + * EDIT_DATE than this will be counted + * @return int number of posts + */ + public function getThreadPostCount($thread_id, $timestamp = 0) + { + $db = $this->db; + $sql = "SELECT COUNT(DISTINCT GI.ID) AS NUM FROM GROUP_ITEM GI + WHERE GI.PARENT_ID = ? AND GI.PUBDATE > ?"; + $result = $db->execute($sql, [$thread_id, $timestamp]); if (!$result) { return 0; } diff --git a/src/models/ImpressionModel.php b/src/models/ImpressionModel.php index 558c20c52..ca5dbd8ea 100644 --- a/src/models/ImpressionModel.php +++ b/src/models/ImpressionModel.php @@ -160,8 +160,51 @@ class ImpressionModel extends Model return $rows; } /** - * @param int $user_id - * @param array $group_ids + * Returns the most recent timestamp of any view impression of a group a + * user has. + * @param int $user_id want most recent impression for + * @return int timstamp of most recent impression + */ + public function mostRecentThreadView($user_id, $thread_id) + { + $db = $this->db; + $sql = "SELECT MAX(UPDATE_TIMESTAMP) AS MOST_RECENT FROM " . + "ITEM_IMPRESSION_SUMMARY WHERE USER_ID = ? AND ITEM_TYPE = " . + C\THREAD_IMPRESSION . " AND ITEM_ID = ? " . + "AND UPDATE_PERIOD = ". C\MOST_RECENT_VIEW; + $result = $db->execute($sql, [$user_id, + $thread_id]); + if (!$result || !($row = $db->fetchArray($result))) { + return 0; + } + return $row["MOST_RECENT"] ?? 0; + } + /** + * Returns the most recent timestamp of any view impression of a group a + * user has. + * @param int $user_id want most recent impression for + * @return int timstamp of most recent impression + */ + public function mostRecentGroupView($user_id) + { + $db = $this->db; + $sql = "SELECT MAX(UPDATE_TIMESTAMP) AS MOST_RECENT FROM " . + "ITEM_IMPRESSION_SUMMARY WHERE USER_ID = $user_id AND ITEM_TYPE = " . + C\GROUP_IMPRESSION . " AND ITEM_ID IN (SELECT GROUP_ID " . + "FROM USER_GROUP WHERE USER_ID = $user_id AND STATUS = ". + C\ACTIVE_STATUS . ") AND UPDATE_PERIOD = ".C\MOST_RECENT_VIEW; + $result = $db->execute($sql); + if (!$result || !($row = $db->fetchArray($result))) { + return 0; + } + return $row["MOST_RECENT"]; + } + /** + * For a list of group_ids that $user_id may belong to, returns an + * array of pairs group_id => timestamp of most recent view + * @param int $user_id user to look up most recent views for + * @param array $group_ids groups to check in + * @return array pairs group_id => timestamp */ public function mostRecentGroupViews($user_id, $group_ids) { @@ -173,10 +216,11 @@ class ImpressionModel extends Model $comma = ","; } $in_clause .= ") "; - $sql = "SELECT ITEM_ID AS GROUP_ID, MAX(VIEW_DATE) AS MOST_RECENT " . - "FROM ITEM_IMPRESSION " . + $sql = "SELECT ITEM_ID AS GROUP_ID, UPDATE_TIMESTAMP AS MOST_RECENT " . + "FROM ITEM_IMPRESSION_SUMMARY " . "WHERE USER_ID = ? AND ITEM_TYPE = " . C\GROUP_IMPRESSION . - " AND ITEM_ID IN $in_clause GROUP BY ITEM_ID"; + " AND ITEM_ID IN $in_clause AND UPDATE_PERIOD = ". + C\MOST_RECENT_VIEW . " AND NUM_VIEWS = 0 GROUP BY ITEM_ID"; $result = $db->execute($sql, [$user_id]); if (!$result) { return []; @@ -501,11 +545,22 @@ class ImpressionModel extends Model !is_numeric($user_id) || !is_numeric($item_id)) { return; } + $dbinfo = ["DBMS" => C\DBMS, "DB_HOST" => C\DB_HOST, + "DB_USER" => C\DB_USER, "DB_PASSWORD" => C\DB_PASSWORD, + "DB_NAME" => C\DB_NAME]; $sql = "INSERT INTO ITEM_IMPRESSION VALUES (?, ?, ?, ?)"; if ($user_id != C\PUBLIC_USER_ID) { $db->execute($sql, [$user_id, $item_id, $type_id, time()]); } $db->execute($sql, [C\PUBLIC_USER_ID, $item_id, $type_id, time()]); + $sql = "DELETE FROM ITEM_IMPRESSION_SUMMARY + WHERE USER_ID=? AND ITEM_ID=? AND ITEM_TYPE=? AND UPDATE_PERIOD = ". + C\MOST_RECENT_VIEW; + $db->execute($sql, [$user_id, $item_id, $type_id]); + $sql = "INSERT INTO ITEM_IMPRESSION_SUMMARY VALUES + (?, ?, ?, " . C\MOST_RECENT_VIEW .", ?, 0, -1, -1)"; + $sql = $db->insertIgnore($sql, $dbinfo); + $db->execute($sql, [$user_id, $item_id, $type_id, time()]); $sql = "UPDATE ITEM_IMPRESSION_SUMMARY SET NUM_VIEWS = NUM_VIEWS + 1 WHERE USER_ID=? AND ITEM_ID=? AND ITEM_TYPE=? AND UPDATE_PERIOD = ". C\FOREVER . " diff --git a/src/views/elements/GroupfeedElement.php b/src/views/elements/GroupfeedElement.php index 38ed77ac8..2e9fb528c 100644 --- a/src/views/elements/GroupfeedElement.php +++ b/src/views/elements/GroupfeedElement.php @@ -54,7 +54,7 @@ class GroupfeedElement extends Element implements CrawlConstants $logged_in = !empty($data["ADMIN"]); $is_status = isset($data['STATUS']); $is_api = !empty($data['API']); - $token = ($logged_in) ? C\CSRF_TOKEN . "=". + $token = ($logged_in) ? C\CSRF_TOKEN . "=" . $data[C\CSRF_TOKEN] : ""; $base_query = B\feedsUrl("", "", true, $data['CONTROLLER']) . $token; diff --git a/src/views/elements/SocialcontrolsElement.php b/src/views/elements/SocialcontrolsElement.php index abfcefdcd..976304047 100644 --- a/src/views/elements/SocialcontrolsElement.php +++ b/src/views/elements/SocialcontrolsElement.php @@ -64,7 +64,7 @@ class SocialcontrolsElement extends Element <?=$icon_helper->renderButton($messages_url, 'messages', $data['UNREAD_MESSAGES'] ?? "");?> <?=$icon_helper->renderButton($feed_url ."&v=ungrouped", - 'combined_discussions', $data['UNREAD_DISCUSSIONS'] ?? "");?> + 'combined_discussions', $data['UNREAD_POSTS'] ?? "");?> <?=$icon_helper->renderButton($feed_url, 'groups', $data['NUM_GROUPS'] ?? "");?> <?=$icon_helper->renderButton($group_url ."&browse=true", diff --git a/src/views/elements/UsermessagesElement.php b/src/views/elements/UsermessagesElement.php index bf46ae708..7a2357cdd 100644 --- a/src/views/elements/UsermessagesElement.php +++ b/src/views/elements/UsermessagesElement.php @@ -134,7 +134,11 @@ class UsermessagesElement extends Element implements CrawlConstants ?> <div class="contact-detail <?=$selected?>" tabindex="0" onclick="javascript:elt('contact-id-<?=$contact_id - ?>').click()"> + ?>').click()"><?php + if ($contact_info['NUM_UNREAD_MESSAGES'] > 0) {?> + <div class='unread-messages'><?= + $contact_info['NUM_UNREAD_MESSAGES'] ?></div><?php + }?> <div class="contact-detail-inner"> <img class="message-icon" src="<?= $contact_info["ICON_URL"]; ?>" diff --git a/src/views/helpers/GrouplistHelper.php b/src/views/helpers/GrouplistHelper.php index 839a689c8..5acddaa64 100644 --- a/src/views/helpers/GrouplistHelper.php +++ b/src/views/helpers/GrouplistHelper.php @@ -166,8 +166,9 @@ class GrouplistHelper extends Helper e(" "); $icon_helper->renderButton( htmlentities(B\feedsUrl("group", $group['GROUP_ID'], true, - $controller)) . $token, 'group_feed', "", false, - "media-buttons-container small-margin" , + $controller)) . $token, 'group_feed', + $group['NEW_POSTS'] ?? 0, + false, "media-buttons-container small-margin" , "media-anchor-button small-font", true); e(" "); $icon_helper->renderButton(htmlentities(B\wikiUrl(