diff --git a/framework/core/js/forum/src/components/post-stream.js b/framework/core/js/forum/src/components/post-stream.js index 8aef5a44d..d01d0c7b6 100644 --- a/framework/core/js/forum/src/components/post-stream.js +++ b/framework/core/js/forum/src/components/post-stream.js @@ -49,7 +49,7 @@ class PostStream extends mixin(Component, evented) { m.redraw(true); return promise.then(() => { - anchorScroll(this.$('.item:'+(backwards ? 'last' : 'first')), () => m.redraw(true)); + anchorScroll(this.$('.item:' + (backwards ? 'last' : 'first')), () => m.redraw(true)); this.scrollToIndex(index, noAnimation, backwards).done(this.unpause.bind(this)); }); @@ -69,43 +69,15 @@ class PostStream extends mixin(Component, evented) { return this.goToIndex(this.count() - 1, true); } - /** - Update the stream to reflect any posts that have been added/removed from the - discussion. - */ - sync() { - var addedPosts = this.discussion.addedPosts(); - if (addedPosts) addedPosts.forEach(this.pushPost.bind(this)); - this.discussion.pushAttributes({links: {addedPosts: null}}); - - var removedPosts = this.discussion.removedPosts(); - if (removedPosts) removedPosts.forEach(this.removePost.bind(this)); - this.discussion.pushAttributes({removedPosts: null}); - } - /** Add a post to the end of the stream. Nothing will be done if the end of the stream is not visible. */ - pushPost(post) { - if (this.visibleEnd >= this.count() - 1 && this.posts.indexOf(post) === -1) { - this.posts.push(post); this.visibleEnd++; } } /** - Search for and remove a specific post from the stream. Nothing will be done - if the post is not visible. - */ - removePost(id) { - this.posts.some((item, i) => { - if (item && item.id() == id) { - this.posts.splice(i, 1); - this.visibleEnd--; - return true; - } - }); } /** @@ -127,7 +99,6 @@ class PostStream extends mixin(Component, evented) { Set up the stream with the given array of posts. */ setup(posts) { - this.posts = posts; this.visibleStart = posts.length ? this.discussion.postIds().indexOf(posts[0].id()) : 0; this.visibleEnd = this.visibleStart + posts.length; } @@ -137,11 +108,6 @@ class PostStream extends mixin(Component, evented) { */ clear(start, end) { this.visibleStart = start || 0; - this.visibleEnd = Math.min(this.count(), end || this.constructor.loadCount); - this.posts = []; - for (var i = this.visibleStart; i < this.visibleEnd; i++) { - this.posts.push(null); - } } /** @@ -157,7 +123,6 @@ class PostStream extends mixin(Component, evented) { var lastTime; return m('div.discussion-posts.posts', {config: this.onload.bind(this)}, - this.posts.map((post, i) => { var content; var attributes = {}; attributes['data-index'] = this.visibleStart + i; @@ -172,7 +137,6 @@ class PostStream extends mixin(Component, evented) { attributes['data-number'] = post.number(); var dt = post.time() - lastTime; - if (dt > 1000 * 60 * 60 * 24 * 4) { content = [ m('div.time-gap', m('span', moment.duration(dt).humanize(), ' later')), content @@ -193,7 +157,6 @@ class PostStream extends mixin(Component, evented) { this.visibleEnd === this.count() && (!app.session.user() || this.discussion.canReply()) && !app.composingReplyTo(this.discussion) - ? m('div.item', ReplyPlaceholder.component({discussion: this.discussion})) : '' ); } @@ -233,7 +196,6 @@ class PostStream extends mixin(Component, evented) { var marginTop = this.getMarginTop(); var viewportHeight = $(window).height() - marginTop; var viewportTop = top + marginTop; - var loadAheadDistance = viewportHeight; if (this.visibleStart > 0) { var $item = this.$('.item[data-index='+this.visibleStart+']'); @@ -262,17 +224,7 @@ class PostStream extends mixin(Component, evented) { var start = this.visibleEnd; var end = this.visibleEnd = this.sanitizeIndex(this.visibleEnd + this.constructor.loadCount); - for (var i = start; i < end; i++) { - this.posts.push(null); - } - - // If the posts which are two pages back from the page we're currently - // loading still haven't loaded, we can assume that the user is scrolling - // pretty fast. Thus, we will unload them. var twoPagesAway = start - this.constructor.loadCount * 2; - if (twoPagesAway >= 0 && !this.posts[twoPagesAway - this.visibleStart]) { - this.posts.splice(0, twoPagesAway + this.constructor.loadCount - this.visibleStart); - this.visibleStart = twoPagesAway + this.constructor.loadCount; clearTimeout(this.loadPageTimeouts[twoPagesAway]); } @@ -286,16 +238,7 @@ class PostStream extends mixin(Component, evented) { var end = this.visibleStart; var start = this.visibleStart = this.sanitizeIndex(this.visibleStart - this.constructor.loadCount); - for (var i = start; i < end; i++) { - this.posts.unshift(null); - } - - // If the posts which are two pages back from the page we're currently - // loading still haven't loaded, we can assume that the user is scrolling - // pretty fast. Thus, we will unload them. var twoPagesAway = start + this.constructor.loadCount * 2; - if (twoPagesAway <= this.count() && !this.posts[twoPagesAway - this.visibleStart]) { - this.posts.splice(twoPagesAway - this.visibleStart); this.visibleEnd = twoPagesAway; clearTimeout(this.loadPageTimeouts[twoPagesAway]); } @@ -310,8 +253,6 @@ class PostStream extends mixin(Component, evented) { var redraw = () => { if (start < this.visibleStart || end > this.visibleEnd) return; - var anchorIndex = backwards && $(window).scrollTop() > 0 ? this.visibleEnd - 1 : this.visibleStart; - anchorScroll(this.$('.item[data-index='+anchorIndex+']'), () => m.redraw(true)); this.unpause(); }; @@ -332,10 +273,7 @@ class PostStream extends mixin(Component, evented) { clearing it. */ loadRange(start, end) { - return app.store.find('posts', this.discussion.postIds().slice(start, end)).then(posts => { - if (start < this.visibleStart || end > this.visibleEnd) return; - this.posts.splice.apply(this.posts, [start - this.visibleStart, end - start].concat(posts)); }); } @@ -345,7 +283,6 @@ class PostStream extends mixin(Component, evented) { resolved immediately. */ loadNearNumber(number) { - if (this.posts.some(post => post.number() == number)) { return m.deferred().resolve().promise; } @@ -372,9 +309,6 @@ class PostStream extends mixin(Component, evented) { this.clear(start, end); - var ids = this.discussion.postIds().slice(start, end); - - return app.store.find('posts', ids).then(this.setup.bind(this)); } /** @@ -419,7 +353,6 @@ class PostStream extends mixin(Component, evented) { would consider a post to be the first one visible. */ getMarginTop() { - return this.$() && $('.global-header').outerHeight() + parseInt(this.$().css('margin-top')); } /** diff --git a/framework/core/js/forum/src/components/reply-composer.js b/framework/core/js/forum/src/components/reply-composer.js index 5336a9c1e..5c3059bdd 100644 --- a/framework/core/js/forum/src/components/reply-composer.js +++ b/framework/core/js/forum/src/components/reply-composer.js @@ -2,7 +2,6 @@ import ItemList from 'flarum/utils/item-list'; import ComposerBody from 'flarum/components/composer-body'; import Alert from 'flarum/components/alert'; import ActionButton from 'flarum/components/action-button'; -import Composer from 'flarum/components/composer'; import icon from 'flarum/helpers/icon'; export default class ReplyComposer extends ComposerBody { @@ -44,26 +43,11 @@ export default class ReplyComposer extends ComposerBody { var data = this.data(); - app.store.createRecord('posts').save(data).then((post) => { - app.composer.hide(); - - discussion.pushAttributes({ - relationships: { - lastUser: post.user(), - lastPost: post - }, - lastTime: post.time(), - lastPostNumber: post.number(), - commentsCount: discussion.commentsCount() + 1, - readTime: post.time(), - readNumber: post.number() - }); - discussion.data().relationships.posts.data.push({type: 'posts', id: post.id()}); - + app.store.createRecord('posts').save(data).then(post => { // If we're currently viewing the discussion which this reply was made // in, then we can add the post to the end of the post stream. - if (app.current && app.current.discussion && app.current.discussion().id() === discussion.id()) { - app.current.stream.pushPost(post); + if (app.viewingDiscussion(discussion)) { + app.current.stream.update(); m.route(app.route('discussion.near', { id: discussion.id(), slug: discussion.slug(), @@ -89,6 +73,8 @@ export default class ReplyComposer extends ComposerBody { }) ); } + + app.composer.hide(); }, errors => { this.loading(false); m.redraw(); diff --git a/framework/core/js/forum/src/initializers/discussion-controls.js b/framework/core/js/forum/src/initializers/discussion-controls.js index f83f05a3a..e436a9ec3 100644 --- a/framework/core/js/forum/src/initializers/discussion-controls.js +++ b/framework/core/js/forum/src/initializers/discussion-controls.js @@ -55,20 +55,21 @@ export default function(app) { app.history.back(); } } - } + }; Discussion.prototype.renameAction = function() { - var currentTitle = this.title(); - var title = prompt('Enter a new title for this discussion:', currentTitle); + const currentTitle = this.title(); + const title = prompt('Enter a new title for this discussion:', currentTitle); + if (title && title !== currentTitle) { - this.save({title}).then(discussion => { - if (app.current instanceof DiscussionPage) { - app.current.stream.sync(); + this.save({title}).then(() => { + if (app.viewingDiscussion(this)) { + app.current.stream.update(); } m.redraw(); }); } - } + }; Discussion.prototype.userControls = function(context) { var items = new ItemList(); diff --git a/framework/core/js/forum/src/initializers/post-controls.js b/framework/core/js/forum/src/initializers/post-controls.js index 2a96aa9cb..fcc2b149a 100644 --- a/framework/core/js/forum/src/initializers/post-controls.js +++ b/framework/core/js/forum/src/initializers/post-controls.js @@ -23,10 +23,7 @@ export default function(app) { function deleteAction() { this.delete(); - // this.discussion().pushAttributes({removedPosts: [this.id()]}); - if (app.current instanceof DiscussionPage) { - app.current.stream.removePost(this.id()); - } + this.discussion().removePost(this.id()); } Post.prototype.userControls = function(context) { diff --git a/framework/core/js/lib/models/discussion.js b/framework/core/js/lib/models/discussion.js index 4d9f53fa5..363e658f3 100644 --- a/framework/core/js/lib/models/discussion.js +++ b/framework/core/js/lib/models/discussion.js @@ -27,6 +27,20 @@ class Discussion extends Model { // } } + removePost(id) { + const relationships = this.data().relationships; + const posts = relationships && relationships.posts; + + if (posts) { + posts.data.some((data, i) => { + if (id === data.id) { + posts.data.splice(i, 1); + return true; + } + }); + } + } + unreadCount() { var user = app.session.user(); if (user && user.readTime() < this.lastTime()) { diff --git a/framework/core/src/Api/Actions/Discussions/UpdateAction.php b/framework/core/src/Api/Actions/Discussions/UpdateAction.php index 991bb07a4..16048c55b 100644 --- a/framework/core/src/Api/Actions/Discussions/UpdateAction.php +++ b/framework/core/src/Api/Actions/Discussions/UpdateAction.php @@ -85,6 +85,17 @@ class UpdateAction extends SerializeResourceAction $discussion = $state->discussion; } + if ($posts = $discussion->getModifiedPosts()) { + $discussion->posts_ids = $discussion->postsVisibleTo($actor)->orderBy('time')->lists('id'); + + $discussion->posts = array_filter($posts, function ($post) { + return $post->exists; + }); + + $request->include = array_merge($request->include, ['posts']); + $request->link = array_merge($request->include, ['posts', 'posts.discussion', 'posts.user']); + } + return $discussion; } } diff --git a/framework/core/src/Api/Actions/Posts/CreateAction.php b/framework/core/src/Api/Actions/Posts/CreateAction.php index 2565a7717..8a3eeac1f 100644 --- a/framework/core/src/Api/Actions/Posts/CreateAction.php +++ b/framework/core/src/Api/Actions/Posts/CreateAction.php @@ -22,13 +22,14 @@ class CreateAction extends BaseCreateAction * @inheritdoc */ public static $include = [ - 'user' => true + 'user' => true, + 'discussion' => true ]; /** * @inheritdoc */ - public static $link = []; + public static $link = ['discussion.posts']; /** * @inheritdoc @@ -84,6 +85,9 @@ class CreateAction extends BaseCreateAction ); } + $discussion = $post->discussion; + $discussion->posts_ids = $discussion->postsVisibleTo($actor)->orderBy('time')->lists('id'); + return $post; } } diff --git a/framework/core/src/Core/Discussions/Discussion.php b/framework/core/src/Core/Discussions/Discussion.php index b07679ab9..e30ee73e7 100755 --- a/framework/core/src/Core/Discussions/Discussion.php +++ b/framework/core/src/Core/Discussions/Discussion.php @@ -46,6 +46,13 @@ class Discussion extends Model 'last_post_number' => 'integer' ]; + /** + * An array of posts that have been modified during this request. + * + * @var array + */ + protected $modifiedPosts = []; + /** * The attributes that should be mutated to dates. * @@ -140,7 +147,7 @@ class Discussion extends Model /** * Set the discussion's start post details. * - * @param \Flarum\Core\Posts\Post $post + * @param Post $post * @return $this */ public function setStartPost(Post $post) @@ -155,7 +162,7 @@ class Discussion extends Model /** * Set the discussion's last post details. * - * @param \Flarum\Core\Posts\Post $post + * @param Post $post * @return $this */ public function setLastPost(Post $post) @@ -214,16 +221,28 @@ class Discussion extends Model * DiscussionRenamedPost, and delete if the title has been reverted * completely.) * - * @param \Flarum\Core\Posts\Post $post The post to save. - * @return \Flarum\Core\Posts\Post The resulting post. It may or may not be - * the same post as was originally intended to be saved. It also may not - * exist, if the merge logic resulted in deletion. + * @param MergeablePost $post The post to save. + * @return Post The resulting post. It may or may not be the same post as + * was originally intended to be saved. It also may not exist, if the + * merge logic resulted in deletion. */ public function mergePost(MergeablePost $post) { $lastPost = $this->posts()->latest('time')->first(); - return $post->saveAfter($lastPost); + $post = $post->saveAfter($lastPost); + + return $this->modifiedPosts[] = $post; + } + + /** + * Get the posts that have been modified during this request. + * + * @return array + */ + public function getModifiedPosts() + { + return $this->modifiedPosts; } /** diff --git a/framework/core/src/Core/Posts/CommentPost.php b/framework/core/src/Core/Posts/CommentPost.php index 7113091dd..e0f03544e 100755 --- a/framework/core/src/Core/Posts/CommentPost.php +++ b/framework/core/src/Core/Posts/CommentPost.php @@ -129,26 +129,6 @@ class CommentPost extends Post return $value; } - /** - * Define the relationship with the user who edited the post. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function editUser() - { - return $this->belongsTo('Flarum\Core\Users\User', 'edit_user_id'); - } - - /** - * Define the relationship with the user who hid the post. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function hideUser() - { - return $this->belongsTo('Flarum\Core\Users\User', 'hide_user_id'); - } - /** * Get text formatter instance. * diff --git a/framework/core/src/Core/Posts/Post.php b/framework/core/src/Core/Posts/Post.php index e852340c5..64650b459 100755 --- a/framework/core/src/Core/Posts/Post.php +++ b/framework/core/src/Core/Posts/Post.php @@ -111,6 +111,26 @@ class Post extends Model return $this->belongsTo('Flarum\Core\Users\User', 'user_id'); } + /** + * Define the relationship with the user who edited the post. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function editUser() + { + return $this->belongsTo('Flarum\Core\Users\User', 'edit_user_id'); + } + + /** + * Define the relationship with the user who hid the post. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function hideUser() + { + return $this->belongsTo('Flarum\Core\Users\User', 'hide_user_id'); + } + /** * Get all posts, regardless of their type, by removing the * `RegisteredTypesScope` global scope constraints applied on this model. diff --git a/framework/core/src/Forum/Actions/DiscussionAction.php b/framework/core/src/Forum/Actions/DiscussionAction.php index 5851691e1..4efcfbb58 100644 --- a/framework/core/src/Forum/Actions/DiscussionAction.php +++ b/framework/core/src/Forum/Actions/DiscussionAction.php @@ -8,7 +8,7 @@ class DiscussionAction extends IndexAction { $response = $this->apiClient->send(app('flarum.actor'), 'Flarum\Api\Actions\Discussions\ShowAction', [ 'id' => $routeParams['id'], - 'near' => $routeParams['near'] + 'page.near' => $routeParams['near'] ]); // TODO: return an object instead of an array?