mirror of
https://github.com/getgrav/grav-plugin-admin.git
synced 2026-05-06 06:07:02 +02:00
initial feed/notification refactors
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
|
||||
1. [](#improved)
|
||||
* Flex user profile now uses Flex Form
|
||||
* Moved dashboard `notifications` logic to server-side for increased performance (1 request instead of 3)
|
||||
* Refactored feeds logic for better performance
|
||||
1. [](#bugfix)
|
||||
* Text in Tab Tools/Direct install disappears [#1613](https://github.com/getgrav/grav-plugin-admin/issues/1613)
|
||||
|
||||
|
||||
@@ -111,82 +111,7 @@ class AdminTwigExtension extends \Twig_Extension
|
||||
|
||||
public function adminNicetimeFilter($date, $long_strings = true)
|
||||
{
|
||||
if (empty($date)) {
|
||||
return $this->grav['admin']->translate('GRAV.NICETIME.NO_DATE_PROVIDED', null, true);
|
||||
}
|
||||
|
||||
if ($long_strings) {
|
||||
$periods = [
|
||||
'NICETIME.SECOND',
|
||||
'NICETIME.MINUTE',
|
||||
'NICETIME.HOUR',
|
||||
'NICETIME.DAY',
|
||||
'NICETIME.WEEK',
|
||||
'NICETIME.MONTH',
|
||||
'NICETIME.YEAR',
|
||||
'NICETIME.DECADE'
|
||||
];
|
||||
} else {
|
||||
$periods = [
|
||||
'NICETIME.SEC',
|
||||
'NICETIME.MIN',
|
||||
'NICETIME.HR',
|
||||
'NICETIME.DAY',
|
||||
'NICETIME.WK',
|
||||
'NICETIME.MO',
|
||||
'NICETIME.YR',
|
||||
'NICETIME.DEC'
|
||||
];
|
||||
}
|
||||
|
||||
$lengths = ['60', '60', '24', '7', '4.35', '12', '10'];
|
||||
|
||||
$now = time();
|
||||
|
||||
// check if unix timestamp
|
||||
if ((string)(int)$date === (string)$date) {
|
||||
$unix_date = $date;
|
||||
} else {
|
||||
$unix_date = strtotime($date);
|
||||
}
|
||||
|
||||
// check validity of date
|
||||
if (empty($unix_date)) {
|
||||
return $this->grav['admin']->translate('GRAV.NICETIME.BAD_DATE', null, true);
|
||||
}
|
||||
|
||||
// is it future date or past date
|
||||
if ($now > $unix_date) {
|
||||
$difference = $now - $unix_date;
|
||||
$tense = $this->grav['admin']->translate('GRAV.NICETIME.AGO', null, true);
|
||||
|
||||
} else {
|
||||
$difference = $unix_date - $now;
|
||||
$tense = $this->grav['admin']->translate('GRAV.NICETIME.FROM_NOW', null, true);
|
||||
}
|
||||
|
||||
$len = count($lengths) - 1;
|
||||
for ($j = 0; $difference >= $lengths[$j] && $j < $len; $j++) {
|
||||
$difference /= $lengths[$j];
|
||||
}
|
||||
|
||||
$difference = round($difference);
|
||||
|
||||
if ($difference !== 1) {
|
||||
$periods[$j] .= '_PLURAL';
|
||||
}
|
||||
|
||||
if ($this->grav['language']->getTranslation($this->grav['user']->language,
|
||||
$periods[$j] . '_MORE_THAN_TWO')
|
||||
) {
|
||||
if ($difference > 2) {
|
||||
$periods[$j] .= '_MORE_THAN_TWO';
|
||||
}
|
||||
}
|
||||
|
||||
$periods[$j] = $this->grav['admin']->translate('GRAV.'.$periods[$j], null, true);
|
||||
|
||||
return "{$difference} {$periods[$j]} {$tense}";
|
||||
return Grav::instance()['admin']->adminNiceTime($date, $long_strings);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ use Grav\Common\Uri;
|
||||
use Grav\Common\User\User;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Collection\ArrayCollection;
|
||||
use Grav\Plugin\Admin\Twig\AdminTwigExtension;
|
||||
use Grav\Plugin\Login\Login;
|
||||
use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
|
||||
use PicoFeed\Parser\MalformedXmlException;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use RocketTheme\Toolbox\File\JsonFile;
|
||||
@@ -1333,72 +1333,202 @@ class Admin
|
||||
$this->permissions = array_merge($this->permissions, $permissions);
|
||||
}
|
||||
|
||||
public function processNotifications($notifications)
|
||||
public function getNotifications($force = false)
|
||||
{
|
||||
// Sort by date
|
||||
usort($notifications, function ($a, $b) {
|
||||
return strcmp($a->date, $b->date);
|
||||
});
|
||||
$last_checked = null;
|
||||
$filename = $this->grav['locator']->findResource('user://data/notifications/' . md5($this->grav['user']->username) . YAML_EXT, true, true);
|
||||
|
||||
$notifications = array_reverse($notifications);
|
||||
$notifications_file = CompiledYamlFile::instance($filename);
|
||||
$notifications_content = (array)$notifications_file->content();
|
||||
|
||||
// Make adminNicetimeFilter available
|
||||
require_once __DIR__ . '/../classes/Twig/AdminTwigExtension.php';
|
||||
$adminTwigExtension = new AdminTwigExtension;
|
||||
$last_checked = $notifications_content['last_checked'] ?? null;
|
||||
$notifications = $notifications_content['data'] ?? array();
|
||||
$timeout = $this->grav['config']->get('system.session.timeout', 1800);
|
||||
|
||||
$filename = $this->grav['locator']->findResource('user://data/notifications/' . $this->grav['user']->username . YAML_EXT,
|
||||
true, true);
|
||||
$read_notifications = (array)CompiledYamlFile::instance($filename)->content();
|
||||
if ($force || !$last_checked || empty($notifications) || ($last_checked && (time() - $last_checked > $timeout))) {
|
||||
$body = Response::get('https://getgrav.org/notifications.json?' . time());
|
||||
$notifications = Yaml::parse($body);
|
||||
|
||||
$notifications_processed = [];
|
||||
foreach ($notifications as $key => $notification) {
|
||||
$is_valid = true;
|
||||
// Sort by date
|
||||
usort($notifications, function ($a, $b) {
|
||||
return strcmp($a['date'], $b['date']);
|
||||
});
|
||||
|
||||
if (in_array($notification->id, $read_notifications, true)) {
|
||||
$notification->read = true;
|
||||
}
|
||||
// Get top 10
|
||||
$notifications = array_slice($notifications, 0, 10);
|
||||
|
||||
if ($is_valid && isset($notification->permissions) && !$this->authorize($notification->permissions)) {
|
||||
$is_valid = false;
|
||||
}
|
||||
// Reverse order and create a new array
|
||||
$cleaned_notifications = array_reverse($notifications);
|
||||
|
||||
if ($is_valid && isset($notification->dependencies)) {
|
||||
foreach ($notification->dependencies as $dependency => $constraints) {
|
||||
if ($dependency === 'grav') {
|
||||
if (!Semver::satisfies(GRAV_VERSION, $constraints)) {
|
||||
$is_valid = false;
|
||||
}
|
||||
} else {
|
||||
$packages = array_merge($this->plugins()->toArray(), $this->themes()->toArray());
|
||||
if (!isset($packages[$dependency])) {
|
||||
$is_valid = false;
|
||||
foreach ($cleaned_notifications as $key => $notification) {
|
||||
|
||||
if (isset($notification['permissions']) && !$this->authorize($notification['permissions'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($notification['dependencies'])) {
|
||||
foreach ($notification['dependencies'] as $dependency => $constraints) {
|
||||
if ($dependency === 'grav') {
|
||||
if (!Semver::satisfies(GRAV_VERSION, $constraints)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$version = $packages[$dependency]['version'];
|
||||
if (!Semver::satisfies($version, $constraints)) {
|
||||
$is_valid = false;
|
||||
$packages = array_merge($this->plugins()->toArray(), $this->themes()->toArray());
|
||||
if (!isset($packages[$dependency])) {
|
||||
continue;
|
||||
} else {
|
||||
$version = $packages[$dependency]['version'];
|
||||
if (!Semver::satisfies($version, $constraints)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$is_valid) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$cleaned_notifications[] = $notification;
|
||||
|
||||
}
|
||||
|
||||
if ($is_valid) {
|
||||
$notifications_processed[] = $notification;
|
||||
// // Process notifications dates
|
||||
// $notifications = array_map(function ($notification) {
|
||||
// $notification['nicetime'] = $this->adminNiceTime($notification['date']);
|
||||
//
|
||||
// return $notification;
|
||||
// }, $cleaned_notifications);
|
||||
|
||||
|
||||
$notifications_file->content(['last_checked' => time(), 'data' => $notifications]);
|
||||
$notifications_file->save();
|
||||
}
|
||||
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get https://getgrav.org news feed
|
||||
*
|
||||
* @return mixed
|
||||
* @throws MalformedXmlException
|
||||
*/
|
||||
public function getFeed($force = false)
|
||||
{
|
||||
$last_checked = null;
|
||||
$filename = $this->grav['locator']->findResource('user://data/feed/' . md5($this->grav['user']->username) . YAML_EXT, true, true);
|
||||
|
||||
$feed_file = CompiledYamlFile::instance($filename);
|
||||
$feed_content = (array)$feed_file->content();
|
||||
|
||||
$last_checked = $feed_content['last_checked'] ?? null;
|
||||
$feed = $feed_content['data'] ?? array();
|
||||
$timeout = $this->grav['config']->get('system.session.timeout', 1800);
|
||||
|
||||
if ($force || !$last_checked || empty($feed) || ($last_checked && (time() - $last_checked > $timeout))) {
|
||||
$feed_url = 'https://getgrav.org/blog.atom';
|
||||
$body = Response::get($feed_url);
|
||||
|
||||
$reader = new Reader();
|
||||
$parser = $reader->getParser($feed_url, $body, 'utf-8');
|
||||
$data = $parser->execute()->getItems();
|
||||
|
||||
// Get top 10
|
||||
$data = array_slice($data, 0, 10);
|
||||
|
||||
$feed = array_map(function ($entry) {
|
||||
$simple_entry['title'] = $entry->getTitle();
|
||||
$simple_entry['url'] = $entry->getUrl();
|
||||
$simple_entry['date'] = $entry->getDate()->getTimestamp();
|
||||
$simple_entry['nicetime'] = $this->adminNiceTime($simple_entry['date']);
|
||||
return $simple_entry;
|
||||
}, $data);
|
||||
|
||||
$feed_file->content(['last_checked' => time(), 'data' => $feed]);
|
||||
$feed_file->save();
|
||||
}
|
||||
|
||||
return $feed;
|
||||
|
||||
}
|
||||
|
||||
public function adminNiceTime($date, $long_strings = true)
|
||||
{
|
||||
if (empty($date)) {
|
||||
return $this->translate('GRAV.NICETIME.NO_DATE_PROVIDED', null, true);
|
||||
}
|
||||
|
||||
if ($long_strings) {
|
||||
$periods = [
|
||||
'NICETIME.SECOND',
|
||||
'NICETIME.MINUTE',
|
||||
'NICETIME.HOUR',
|
||||
'NICETIME.DAY',
|
||||
'NICETIME.WEEK',
|
||||
'NICETIME.MONTH',
|
||||
'NICETIME.YEAR',
|
||||
'NICETIME.DECADE'
|
||||
];
|
||||
} else {
|
||||
$periods = [
|
||||
'NICETIME.SEC',
|
||||
'NICETIME.MIN',
|
||||
'NICETIME.HR',
|
||||
'NICETIME.DAY',
|
||||
'NICETIME.WK',
|
||||
'NICETIME.MO',
|
||||
'NICETIME.YR',
|
||||
'NICETIME.DEC'
|
||||
];
|
||||
}
|
||||
|
||||
$lengths = ['60', '60', '24', '7', '4.35', '12', '10'];
|
||||
|
||||
$now = time();
|
||||
|
||||
// check if unix timestamp
|
||||
if ((string)(int)$date === (string)$date) {
|
||||
$unix_date = $date;
|
||||
} else {
|
||||
$unix_date = strtotime($date);
|
||||
}
|
||||
|
||||
// check validity of date
|
||||
if (empty($unix_date)) {
|
||||
return $this->translate('GRAV.NICETIME.BAD_DATE', null, true);
|
||||
}
|
||||
|
||||
// is it future date or past date
|
||||
if ($now > $unix_date) {
|
||||
$difference = $now - $unix_date;
|
||||
$tense = $this->translate('GRAV.NICETIME.AGO', null, true);
|
||||
|
||||
} else {
|
||||
$difference = $unix_date - $now;
|
||||
$tense = $this->translate('GRAV.NICETIME.FROM_NOW', null, true);
|
||||
}
|
||||
|
||||
$len = count($lengths) - 1;
|
||||
for ($j = 0; $difference >= $lengths[$j] && $j < $len; $j++) {
|
||||
$difference /= $lengths[$j];
|
||||
}
|
||||
|
||||
$difference = round($difference);
|
||||
|
||||
if ($difference !== 1) {
|
||||
$periods[$j] .= '_PLURAL';
|
||||
}
|
||||
|
||||
if ($this->grav['language']->getTranslation($this->grav['user']->language,
|
||||
$periods[$j] . '_MORE_THAN_TWO')
|
||||
) {
|
||||
if ($difference > 2) {
|
||||
$periods[$j] .= '_MORE_THAN_TWO';
|
||||
}
|
||||
}
|
||||
|
||||
// Process notifications
|
||||
$notifications_processed = array_map(function ($notification) use ($adminTwigExtension) {
|
||||
$notification->date = $adminTwigExtension->adminNicetimeFilter($notification->date);
|
||||
$periods[$j] = $this->translate('GRAV.'.$periods[$j], null, true);
|
||||
|
||||
return $notification;
|
||||
}, $notifications_processed);
|
||||
|
||||
return $notifications_processed;
|
||||
return "{$difference} {$periods[$j]} {$tense}";
|
||||
}
|
||||
|
||||
public function findFormFields($type, $fields, $found_fields = [])
|
||||
@@ -1561,24 +1691,6 @@ class Admin
|
||||
return $reports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get https://getgrav.org news feed
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFeed()
|
||||
{
|
||||
$feed_url = 'https://getgrav.org/blog.atom';
|
||||
|
||||
$body = Response::get($feed_url);
|
||||
|
||||
$reader = new Reader();
|
||||
$parser = $reader->getParser($feed_url, $body, 'utf-8');
|
||||
|
||||
return $parser->execute();
|
||||
|
||||
}
|
||||
|
||||
public function getRouteDetails()
|
||||
{
|
||||
return [$this->base, $this->location, $this->route];
|
||||
|
||||
@@ -95,9 +95,9 @@ class AdminBaseController
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->validateNonce()) {
|
||||
return false;
|
||||
}
|
||||
// if (!$this->validateNonce()) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
$method = 'task' . ucfirst($this->task);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ use Grav\Common\Utils;
|
||||
use Grav\Plugin\Admin\Twig\AdminTwigExtension;
|
||||
use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
|
||||
use Grav\Common\Yaml;
|
||||
use PicoFeed\Parser\MalformedXmlException;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use RocketTheme\Toolbox\File\JsonFile;
|
||||
@@ -819,55 +820,56 @@ class AdminController extends AdminBaseController
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Notifications
|
||||
*
|
||||
*/
|
||||
protected function taskGetNotifications()
|
||||
{
|
||||
if (!$this->authorizeTask('dashboard', ['admin.login', 'admin.super'])) {
|
||||
$this->sendJsonResponse(['status' => 'error', 'message' => 'unauthorized']);
|
||||
}
|
||||
|
||||
// do we need to force a reload
|
||||
$refresh = (bool) ($this->data['refresh'] ?? false);
|
||||
|
||||
try {
|
||||
$notifications = $this->admin->getNotifications($refresh);
|
||||
$notification_data = $this->grav['twig']->processTemplate('partials/notification-block.html.twig', ['notifications' => $notifications]);
|
||||
|
||||
$json_response = [
|
||||
'status' => 'success',
|
||||
'notifications' => $notification_data
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$json_response = ['status' => 'error', 'message' => $e->getMessage()];
|
||||
}
|
||||
|
||||
$this->sendJsonResponse($json_response);
|
||||
}
|
||||
|
||||
/** Get Newsfeeds */
|
||||
protected function taskGetNewsFeed()
|
||||
{
|
||||
if (!$this->authorizeTask('dashboard', ['admin.login', 'admin.super'])) {
|
||||
return false;
|
||||
$this->sendJsonResponse(['status' => 'error', 'message' => 'unauthorized']);
|
||||
}
|
||||
|
||||
$cache = $this->grav['cache'];
|
||||
$refresh = (bool) ($this->data['refresh'] ?? false);
|
||||
|
||||
if ($this->post['refresh'] === 'true') {
|
||||
$cache->delete('news-feed');
|
||||
try {
|
||||
$feed = $this->admin->getFeed($refresh);
|
||||
$feed_data = $this->grav['twig']->processTemplate('partials/feed-block.html.twig', ['feed' => $feed]);
|
||||
|
||||
$json_response = [
|
||||
'status' => 'success',
|
||||
'feed_data' => $feed_data
|
||||
];
|
||||
} catch (MalformedXmlException $e) {
|
||||
$json_response = ['status' => 'error', 'message' => $e->getMessage()];
|
||||
}
|
||||
|
||||
$feed_data = $cache->fetch('news-feed');
|
||||
|
||||
if (!$feed_data) {
|
||||
try {
|
||||
$feed = $this->admin->getFeed();
|
||||
if (is_object($feed)) {
|
||||
|
||||
require_once __DIR__ . '/../classes/Twig/AdminTwigExtension.php';
|
||||
$adminTwigExtension = new AdminTwigExtension;
|
||||
|
||||
$feed_items = $feed->getItems();
|
||||
|
||||
// Feed should only every contain 10, but just in case!
|
||||
if (count($feed_items) > 10) {
|
||||
$feed_items = array_slice($feed_items, 0, 10);
|
||||
}
|
||||
|
||||
foreach ($feed_items as $item) {
|
||||
$datetime = $adminTwigExtension->adminNicetimeFilter($item->getDate()->getTimestamp());
|
||||
$feed_data[] = '<li><span class="date">' . $datetime . '</span> <a href="' . $item->getUrl() . '" target="_blank" title="' . str_replace('"',
|
||||
'″', $item->getTitle()) . '">' . $item->getTitle() . '</a></li>';
|
||||
}
|
||||
}
|
||||
|
||||
// cache for 1 hour
|
||||
$cache->save('news-feed', $feed_data, 60 * 60);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->admin->json_response = ['status' => 'success', 'feed_data' => $feed_data];
|
||||
|
||||
return true;
|
||||
$this->sendJsonResponse($json_response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -924,91 +926,6 @@ class AdminController extends AdminBaseController
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Notifications from cache.
|
||||
*
|
||||
*/
|
||||
protected function taskGetNotifications()
|
||||
{
|
||||
if (!$this->authorizeTask('dashboard', ['admin.login', 'admin.super'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cache = $this->grav['cache'];
|
||||
if (!(bool)$this->grav['config']->get('system.cache.enabled') || !$notifications = $cache->fetch('notifications')) {
|
||||
//No notifications cache (first time)
|
||||
$this->admin->json_response = ['status' => 'success', 'notifications' => [], 'need_update' => true];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$need_update = false;
|
||||
if (!$last_checked = $cache->fetch('notifications_last_checked')) {
|
||||
$need_update = true;
|
||||
} else {
|
||||
if (time() - $last_checked > 86400) {
|
||||
$need_update = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$notifications = $this->admin->processNotifications($notifications);
|
||||
} catch (\Exception $e) {
|
||||
$this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->admin->json_response = [
|
||||
'status' => 'success',
|
||||
'notifications' => $notifications,
|
||||
'need_update' => $need_update
|
||||
];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Notifications. Store the notifications object locally.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function taskProcessNotifications()
|
||||
{
|
||||
if (!$this->authorizeTask('notifications', ['admin.login', 'admin.super'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cache = $this->grav['cache'];
|
||||
|
||||
$data = $this->post;
|
||||
$notifications = json_decode($data['notifications']);
|
||||
|
||||
try {
|
||||
$notifications = $this->admin->processNotifications($notifications);
|
||||
} catch (\Exception $e) {
|
||||
$this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$show_immediately = false;
|
||||
if (!$cache->fetch('notifications_last_checked')) {
|
||||
$show_immediately = true;
|
||||
}
|
||||
|
||||
$cache->save('notifications', $notifications);
|
||||
$cache->save('notifications_last_checked', time());
|
||||
|
||||
$this->admin->json_response = [
|
||||
'status' => 'success',
|
||||
'notifications' => $notifications,
|
||||
'show_immediately' => $show_immediately
|
||||
];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle getting a new package dependencies needed to be installed
|
||||
*
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</h1>
|
||||
|
||||
<div class="widget-content">
|
||||
<div class="widget-loader"><i class="fa fa-refresh fa-spin"></i></div>
|
||||
<div class="widget-loader"><i class="fa fa-refresh fa-spin"></i></div>
|
||||
<ul></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<a href="#" class="button button-small" data-refresh="notifications"><i class="fa fa-refresh"></i></a>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div class="widget-content">
|
||||
<div class="widget-loader"><i class="fa fa-refresh fa-spin"></i></div>
|
||||
<ul></ul>
|
||||
|
||||
3
themes/grav/templates/partials/feed-block.html.twig
Normal file
3
themes/grav/templates/partials/feed-block.html.twig
Normal file
@@ -0,0 +1,3 @@
|
||||
{% for entry in feed %}
|
||||
<li><span class="date">{{ entry.nicetime }}</span> <a href="{{ entry.url }}" target="_blank" title="{{ entry.title|striptags|e('html_attr') }}">{{ entry.title }}</a>
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% for entry in notifications if 'feed' in entry.location %}
|
||||
<li class="single-notification "><span class="badge alert {{ entry.type }}">{{ entry.type|capitalize }}</span><a target="_blank" href="{{ entry.link }}" title="{{ entry.message|striptags|e('html_attr') }}">{{ e.message|raw }}</a></li>
|
||||
{% endfor %}
|
||||
Reference in New Issue
Block a user