Add infinite scrolling in the notifications list

This commit is contained in:
Toby Zerner 2017-12-13 15:28:54 +10:30
parent 0c1e90719c
commit 3e29761d12
4 changed files with 260 additions and 124 deletions

View File

@ -23964,39 +23964,18 @@ System.register('flarum/components/NotificationList', ['flarum/Component', 'flar
* @type {Boolean} * @type {Boolean}
*/ */
this.loading = false; this.loading = false;
/**
* Whether or not there are more results that can be loaded.
*
* @type {Boolean}
*/
this.moreResults = false;
} }
}, { }, {
key: 'view', key: 'view',
value: function view() { value: function view() {
var groups = []; var pages = app.cache.notifications || [];
if (app.cache.notifications) {
var discussions = {};
// Build an array of discussions which the notifications are related to,
// and add the notifications as children.
app.cache.notifications.forEach(function (notification) {
var subject = notification.subject();
if (typeof subject === 'undefined') return;
// Get the discussion that this notification is related to. If it's not
// directly related to a discussion, it may be related to a post or
// other entity which is related to a discussion.
var discussion = false;
if (subject instanceof Discussion) discussion = subject;else if (subject && subject.discussion) discussion = subject.discussion();
// If the notification is not related to a discussion directly or
// indirectly, then we will assign it to a neutral group.
var key = discussion ? discussion.id() : 0;
discussions[key] = discussions[key] || { discussion: discussion, notifications: [] };
discussions[key].notifications.push(notification);
if (groups.indexOf(discussions[key]) === -1) {
groups.push(discussions[key]);
}
});
}
return m( return m(
'div', 'div',
@ -24023,7 +24002,33 @@ System.register('flarum/components/NotificationList', ['flarum/Component', 'flar
m( m(
'div', 'div',
{ className: 'NotificationList-content' }, { className: 'NotificationList-content' },
groups.length ? groups.map(function (group) { pages.length ? pages.map(function (notifications) {
var groups = [];
var discussions = {};
notifications.forEach(function (notification) {
var subject = notification.subject();
if (typeof subject === 'undefined') return;
// Get the discussion that this notification is related to. If it's not
// directly related to a discussion, it may be related to a post or
// other entity which is related to a discussion.
var discussion = false;
if (subject instanceof Discussion) discussion = subject;else if (subject && subject.discussion) discussion = subject.discussion();
// If the notification is not related to a discussion directly or
// indirectly, then we will assign it to a neutral group.
var key = discussion ? discussion.id() : 0;
discussions[key] = discussions[key] || { discussion: discussion, notifications: [] };
discussions[key].notifications.push(notification);
if (groups.indexOf(discussions[key]) === -1) {
groups.push(discussions[key]);
}
});
return groups.map(function (group) {
var badges = group.discussion && group.discussion.badges().toArray(); var badges = group.discussion && group.discussion.badges().toArray();
return m( return m(
@ -24058,36 +24063,83 @@ System.register('flarum/components/NotificationList', ['flarum/Component', 'flar
}) })
) )
); );
}) : !this.loading ? m( });
}) : '',
this.loading ? m(LoadingIndicator, { className: 'LoadingIndicator--block' }) : pages.length ? '' : m(
'div', 'div',
{ className: 'NotificationList-empty' }, { className: 'NotificationList-empty' },
app.translator.trans('core.forum.notifications.empty_text') app.translator.trans('core.forum.notifications.empty_text')
) : LoadingIndicator.component({ className: 'LoadingIndicator--block' }) )
) )
); );
} }
}, { }, {
key: 'load', key: 'config',
value: function load() { value: function config(isInitialized, context) {
var _this2 = this; var _this2 = this;
if (app.cache.notifications && !app.session.user.newNotificationsCount()) { if (isInitialized) return;
var $notifications = this.$('.NotificationList-content');
var $scrollParent = $notifications.css('overflow') === 'auto' ? $notifications : $(window);
var scrollHandler = function scrollHandler() {
var scrollTop = $scrollParent.scrollTop();
var viewportHeight = $scrollParent.height();
var contentTop = $scrollParent === $notifications ? 0 : $notifications.offset().top;
var contentHeight = $notifications[0].scrollHeight;
if (_this2.moreResults && !_this2.loading && scrollTop + viewportHeight >= contentTop + contentHeight) {
_this2.loadMore();
}
};
$scrollParent.on('scroll', scrollHandler);
context.onunload = function () {
$scrollParent.off('scroll', scrollHandler);
};
}
}, {
key: 'load',
value: function load() {
if (app.session.user.newNotificationsCount()) {
delete app.cache.notifications;
}
if (app.cache.notifications) {
return; return;
} }
app.session.user.pushAttributes({ newNotificationsCount: 0 });
this.loadMore();
}
}, {
key: 'loadMore',
value: function loadMore() {
var _this3 = this;
this.loading = true; this.loading = true;
m.redraw(); m.redraw();
app.store.find('notifications').then(function (notifications) { var params = app.cache.notifications ? { page: { offset: app.cache.notifications.length * 10 } } : null;
app.session.user.pushAttributes({ newNotificationsCount: 0 });
app.cache.notifications = notifications.sort(function (a, b) { return app.store.find('notifications', params).then(this.parseResults.bind(this)).catch(function () {}).then(function () {
return b.time() - a.time(); _this3.loading = false;
});
}).catch(function () {}).then(function () {
_this2.loading = false;
m.redraw(); m.redraw();
}); });
} }
}, {
key: 'parseResults',
value: function parseResults(results) {
app.cache.notifications = app.cache.notifications || [];
app.cache.notifications.push(results);
this.moreResults = !!results.payload.links.next;
return results;
}
}, { }, {
key: 'markAllAsRead', key: 'markAllAsRead',
value: function markAllAsRead() { value: function markAllAsRead() {
@ -24095,9 +24147,11 @@ System.register('flarum/components/NotificationList', ['flarum/Component', 'flar
app.session.user.pushAttributes({ unreadNotificationsCount: 0 }); app.session.user.pushAttributes({ unreadNotificationsCount: 0 });
app.cache.notifications.forEach(function (notification) { app.cache.notifications.forEach(function (notifications) {
notifications.forEach(function (notification) {
return notification.pushAttributes({ isRead: true }); return notification.pushAttributes({ isRead: true });
}); });
});
app.request({ app.request({
url: app.forum.attribute('apiUrl') + '/notifications/read', url: app.forum.attribute('apiUrl') + '/notifications/read',

View File

@ -16,17 +16,39 @@ export default class NotificationList extends Component {
* @type {Boolean} * @type {Boolean}
*/ */
this.loading = false; this.loading = false;
/**
* Whether or not there are more results that can be loaded.
*
* @type {Boolean}
*/
this.moreResults = false;
} }
view() { view() {
const groups = []; const pages = app.cache.notifications || [];
if (app.cache.notifications) { return (
<div className="NotificationList">
<div className="NotificationList-header">
<div className="App-primaryControl">
{Button.component({
className: 'Button Button--icon Button--link',
icon: 'check',
title: app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'),
onclick: this.markAllAsRead.bind(this)
})}
</div>
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('core.forum.notifications.title')}</h4>
</div>
<div className="NotificationList-content">
{pages.length ? pages.map(notifications => {
const groups = [];
const discussions = {}; const discussions = {};
// Build an array of discussions which the notifications are related to, notifications.forEach(notification => {
// and add the notifications as children.
app.cache.notifications.forEach(notification => {
const subject = notification.subject(); const subject = notification.subject();
if (typeof subject === 'undefined') return; if (typeof subject === 'undefined') return;
@ -48,26 +70,8 @@ export default class NotificationList extends Component {
groups.push(discussions[key]); groups.push(discussions[key]);
} }
}); });
}
return ( return groups.map(group => {
<div className="NotificationList">
<div className="NotificationList-header">
<div className="App-primaryControl">
{Button.component({
className: 'Button Button--icon Button--link',
icon: 'check',
title: app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'),
onclick: this.markAllAsRead.bind(this)
})}
</div>
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('core.forum.notifications.title')}</h4>
</div>
<div className="NotificationList-content">
{groups.length
? groups.map(group => {
const badges = group.discussion && group.discussion.badges().toArray(); const badges = group.discussion && group.discussion.badges().toArray();
return ( return (
@ -94,32 +98,71 @@ export default class NotificationList extends Component {
</ul> </ul>
</div> </div>
); );
}) });
: !this.loading }) : ''}
? <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div> {this.loading
: LoadingIndicator.component({className: 'LoadingIndicator--block'})} ? <LoadingIndicator className="LoadingIndicator--block" />
: (pages.length ? '' : <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>)}
</div> </div>
</div> </div>
); );
} }
config(isInitialized, context) {
if (isInitialized) return;
const $notifications = this.$('.NotificationList-content');
const $scrollParent = $notifications.css('overflow') === 'auto' ? $notifications : $(window);
const scrollHandler = () => {
const scrollTop = $scrollParent.scrollTop();
const viewportHeight = $scrollParent.height();
const contentTop = $scrollParent === $notifications ? 0 : $notifications.offset().top;
const contentHeight = $notifications[0].scrollHeight;
if (this.moreResults && !this.loading && scrollTop + viewportHeight >= contentTop + contentHeight) {
this.loadMore();
}
};
$scrollParent.on('scroll', scrollHandler);
context.onunload = () => {
$scrollParent.off('scroll', scrollHandler);
};
}
/** /**
* Load notifications into the application's cache if they haven't already * Load notifications into the application's cache if they haven't already
* been loaded. * been loaded.
*/ */
load() { load() {
if (app.cache.notifications && !app.session.user.newNotificationsCount()) { if (app.session.user.newNotificationsCount()) {
delete app.cache.notifications;
}
if (app.cache.notifications) {
return; return;
} }
app.session.user.pushAttributes({newNotificationsCount: 0});
this.loadMore();
}
/**
* Load the next page of notification results.
*
* @public
*/
loadMore() {
this.loading = true; this.loading = true;
m.redraw(); m.redraw();
app.store.find('notifications') const params = app.cache.notifications ? {page: {offset: app.cache.notifications.length * 10}} : null;
.then(notifications => {
app.session.user.pushAttributes({newNotificationsCount: 0}); return app.store.find('notifications', params)
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time()); .then(this.parseResults.bind(this))
})
.catch(() => {}) .catch(() => {})
.then(() => { .then(() => {
this.loading = false; this.loading = false;
@ -127,6 +170,21 @@ export default class NotificationList extends Component {
}); });
} }
/**
* Parse results and append them to the notification list.
*
* @param {Notification[]} results
* @return {Notification[]}
*/
parseResults(results) {
app.cache.notifications = app.cache.notifications || [];
app.cache.notifications.push(results);
this.moreResults = !!results.payload.links.next;
return results;
}
/** /**
* Mark all of the notifications as read. * Mark all of the notifications as read.
*/ */
@ -135,7 +193,9 @@ export default class NotificationList extends Component {
app.session.user.pushAttributes({unreadNotificationsCount: 0}); app.session.user.pushAttributes({unreadNotificationsCount: 0});
app.cache.notifications.forEach(notification => notification.pushAttributes({isRead: true})); app.cache.notifications.forEach(notifications => {
notifications.forEach(notification => notification.pushAttributes({isRead: true}))
});
app.request({ app.request({
url: app.forum.attribute('apiUrl') + '/notifications/read', url: app.forum.attribute('apiUrl') + '/notifications/read',

View File

@ -6,7 +6,6 @@
.NotificationList-content { .NotificationList-content {
max-height: 70vh; max-height: 70vh;
overflow: auto; overflow: auto;
padding-bottom: 10px;
} }
} }
& .Dropdown-toggle .Button-label { & .Dropdown-toggle .Button-label {

View File

@ -11,6 +11,7 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Api\UrlGenerator;
use Flarum\Core\Discussion; use Flarum\Core\Discussion;
use Flarum\Core\Exception\PermissionDeniedException; use Flarum\Core\Exception\PermissionDeniedException;
use Flarum\Core\Repository\NotificationRepository; use Flarum\Core\Repository\NotificationRepository;
@ -39,16 +40,23 @@ class ListNotificationsController extends AbstractCollectionController
public $limit = 10; public $limit = 10;
/** /**
* @var \Flarum\Core\Repository\NotificationRepository * @var NotificationRepository
*/ */
protected $notifications; protected $notifications;
/** /**
* @param \Flarum\Core\Repository\NotificationRepository $notifications * @var UrlGenerator
*/ */
public function __construct(NotificationRepository $notifications) protected $url;
/**
* @param NotificationRepository $notifications
* @param UrlGenerator $url
*/
public function __construct(NotificationRepository $notifications, UrlGenerator $url)
{ {
$this->notifications = $notifications; $this->notifications = $notifications;
$this->url = $url;
} }
/** /**
@ -68,10 +76,25 @@ class ListNotificationsController extends AbstractCollectionController
$offset = $this->extractOffset($request); $offset = $this->extractOffset($request);
$include = $this->extractInclude($request); $include = $this->extractInclude($request);
$notifications = $this->notifications->findByUser($actor, $limit, $offset) $notifications = $this->notifications->findByUser($actor, $limit + 1, $offset)
->load(array_diff($include, ['subject.discussion'])) ->load(array_diff($include, ['subject.discussion']))
->all(); ->all();
$areMoreResults = false;
if (count($notifications) > $limit) {
array_pop($notifications);
$areMoreResults = true;
}
$document->addPaginationLinks(
$this->url->toRoute('notifications.index'),
$request->getQueryParams(),
$offset,
$limit,
$areMoreResults ? null : 0
);
if (in_array('subject.discussion', $include)) { if (in_array('subject.discussion', $include)) {
$this->loadSubjectDiscussions($notifications); $this->loadSubjectDiscussions($notifications);
} }