mirror of
https://github.com/flarum/core.git
synced 2025-07-30 21:20:24 +02:00
Upgrade to Ember 1.11-beta.1
HTMLBars goodness! Since there was some breakage and a lot of fiddling around to get some things working, I took this opportunity to do a big cleanup of the whole Ember app. I accidentally worked on some new features too :3 Note that the app is still broken right now, pending on https://github.com/emberjs/ember.js/issues/10401 Cleanup: - Restructuring of components - Consolidation of some stuff into mixins, cleanup of some APIs that will be public - Change all instances of .property() / .observes() / .on() to Ember.computed() / Ember.observer() / Ember.on() respectively (I think it is more readable) - More comments - Start conforming to a code style (2 spaces for indentation) New features: - Post hiding/restoring - Mark individual discussions as read by clicking - Clicking on a read discussion jumps to the end - Mark all discussions as read - Progressively mark the discussion as read as the page is scrolled - Unordered list post formatting - Post permalink popup Demo once that Ember regression is fixed!
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
var DiscussionResult = Ember.ObjectProxy.extend({
|
||||
|
||||
relevantPosts: null,
|
||||
|
||||
startPost: null,
|
||||
lastPost: null
|
||||
|
||||
export default Ember.ObjectProxy.extend({
|
||||
relevantPosts: null,
|
||||
startPost: null,
|
||||
lastPost: null
|
||||
});
|
||||
|
||||
export default DiscussionResult;
|
||||
|
@@ -1,48 +1,49 @@
|
||||
import Ember from 'ember';
|
||||
import DS from 'ember-data';
|
||||
|
||||
var Discussion = DS.Model.extend({
|
||||
export default DS.Model.extend({
|
||||
title: DS.attr('string'),
|
||||
slug: Ember.computed('title', function() {
|
||||
return this.get('title').toLowerCase().replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-');
|
||||
}),
|
||||
|
||||
title: DS.attr('string'),
|
||||
content: DS.attr('string'), // only used to save a new discussion
|
||||
startTime: DS.attr('date'),
|
||||
startUser: DS.belongsTo('user'),
|
||||
startPost: DS.belongsTo('post'),
|
||||
|
||||
slug: function() {
|
||||
return this.get('title').toLowerCase().replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-');
|
||||
}.property('title'),
|
||||
|
||||
canReply: DS.attr('boolean'),
|
||||
canEdit: DS.attr('boolean'),
|
||||
canDelete: DS.attr('boolean'),
|
||||
lastTime: DS.attr('date'),
|
||||
lastUser: DS.belongsTo('user'),
|
||||
lastPost: DS.belongsTo('post'),
|
||||
lastPostNumber: DS.attr('number'),
|
||||
|
||||
startTime: DS.attr('date'),
|
||||
startUser: DS.belongsTo('user'),
|
||||
startPost: DS.belongsTo('post'),
|
||||
canReply: DS.attr('boolean'),
|
||||
canEdit: DS.attr('boolean'),
|
||||
canDelete: DS.attr('boolean'),
|
||||
|
||||
lastTime: DS.attr('date'),
|
||||
lastUser: DS.belongsTo('user'),
|
||||
lastPost: DS.belongsTo('post'),
|
||||
lastPostNumber: DS.attr('number'),
|
||||
commentsCount: DS.attr('number'),
|
||||
repliesCount: Ember.computed('commentsCount', function() {
|
||||
return Math.max(0, this.get('commentsCount') - 1);
|
||||
}),
|
||||
|
||||
relevantPosts: DS.hasMany('post'),
|
||||
// The API returns the `posts` relationship as a list of IDs. To hydrate a
|
||||
// post-stream object, we're only interested in obtaining a list of IDs, so
|
||||
// we make it a string and then split it by comma. Instead, we'll put a
|
||||
// relationship on `loadedPosts`.
|
||||
posts: DS.attr('string'),
|
||||
postIds: Ember.computed('posts', function() {
|
||||
var posts = this.get('posts') || '';
|
||||
return posts.split(',');
|
||||
}),
|
||||
loadedPosts: DS.hasMany('post'),
|
||||
relevantPosts: DS.hasMany('post'),
|
||||
|
||||
commentsCount: DS.attr('number'),
|
||||
repliesCount: function() {
|
||||
return Math.max(0, this.get('commentsCount') - 1);
|
||||
}.property('commentsCount'),
|
||||
readTime: DS.attr('date'),
|
||||
readNumber: DS.attr('number'),
|
||||
unreadCount: Ember.computed('lastPostNumber', 'readNumber', 'session.user.readTime', function() {
|
||||
return this.get('session.user.readTime') < this.get('lastTime') ? Math.max(0, this.get('lastPostNumber') - (this.get('readNumber') || 0)) : 0;
|
||||
}),
|
||||
isUnread: Ember.computed.bool('unreadCount'),
|
||||
|
||||
posts: DS.attr('string'),
|
||||
postIds: function() {
|
||||
var posts = this.get('posts') || '';
|
||||
return posts.split(',');
|
||||
}.property('posts'),
|
||||
loadedPosts: DS.hasMany('post'),
|
||||
|
||||
readTime: DS.attr('date'),
|
||||
readNumber: DS.attr('number'),
|
||||
unreadCount: function() {
|
||||
return this.get('lastPostNumber') - this.get('readNumber');
|
||||
}.property('lastPostNumber', 'readNumber'),
|
||||
isUnread: Ember.computed.bool('unreadCount')
|
||||
// Only used to save a new discussion
|
||||
content: DS.attr('string')
|
||||
});
|
||||
|
||||
export default Discussion;
|
||||
|
@@ -1,9 +1,6 @@
|
||||
import DS from 'ember-data';
|
||||
|
||||
export default DS.Model.extend({
|
||||
|
||||
name: DS.attr('string'),
|
||||
|
||||
users: DS.hasMany('group'),
|
||||
|
||||
name: DS.attr('string'),
|
||||
users: DS.hasMany('group'),
|
||||
});
|
||||
|
@@ -1,202 +1,203 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
// The post stream is an object which represents the posts in a discussion as
|
||||
// they are displayed on the discussion page, from top to bottom. ...
|
||||
|
||||
/**
|
||||
The post stream is an object which represents the posts in a discussion as
|
||||
they are displayed on the discussion page, from top to bottom. ...
|
||||
*/
|
||||
export default Ember.ArrayProxy.extend(Ember.Evented, {
|
||||
|
||||
// An array of all of the post IDs, in chronological order, in the discussion.
|
||||
ids: null,
|
||||
// An array of all of the post IDs, in chronological order, in the discussion.
|
||||
ids: null,
|
||||
|
||||
content: null,
|
||||
content: null,
|
||||
|
||||
store: null,
|
||||
discussion: null,
|
||||
store: null,
|
||||
discussion: null,
|
||||
|
||||
postLoadCount: 20,
|
||||
postLoadCount: 20,
|
||||
|
||||
count: Ember.computed.alias('ids.length'),
|
||||
count: Ember.computed.alias('ids.length'),
|
||||
|
||||
loadedCount: function() {
|
||||
return this.get('content').filterBy('content').length;
|
||||
}.property('content.@each'),
|
||||
loadedCount: Ember.computed('content.@each', function() {
|
||||
return this.get('content').filterBy('content').length;
|
||||
}),
|
||||
|
||||
firstLoaded: function() {
|
||||
var first = this.objectAt(0);
|
||||
return first && first.content;
|
||||
}.property('content.@each'),
|
||||
firstLoaded: Ember.computed('content.@each', function() {
|
||||
var first = this.objectAt(0);
|
||||
return first && first.content;
|
||||
}),
|
||||
|
||||
lastLoaded: function() {
|
||||
var last = this.objectAt(this.get('length') - 1);
|
||||
return last && last.content;
|
||||
}.property('content.@each'),
|
||||
lastLoaded: Ember.computed('content.@each', function() {
|
||||
var last = this.objectAt(this.get('length') - 1);
|
||||
return last && last.content;
|
||||
}),
|
||||
|
||||
init: function() {
|
||||
this._super();
|
||||
this.set('ids', Ember.A());
|
||||
this.clear();
|
||||
},
|
||||
init: function() {
|
||||
this._super();
|
||||
this.set('ids', Ember.A());
|
||||
this.clear();
|
||||
},
|
||||
|
||||
setup: function(ids) {
|
||||
// Set our ids to the array provided and reset the content of the
|
||||
// stream to a big gap that covers the amount of posts we now have.
|
||||
this.set('ids', ids);
|
||||
this.clear();
|
||||
},
|
||||
setup: function(ids) {
|
||||
// Set our ids to the array provided and reset the content of the
|
||||
// stream to a big gap that covers the amount of posts we now have.
|
||||
this.set('ids', ids);
|
||||
this.clear();
|
||||
},
|
||||
|
||||
// Clear the contents of the post stream, resetting it to one big gap.
|
||||
clear: function() {
|
||||
var content = Ember.A();
|
||||
content.clear().pushObject(this.makeItem(0, this.get('count') - 1).set('loading', true));
|
||||
this.set('content', content);
|
||||
},
|
||||
// Clear the contents of the post stream, resetting it to one big gap.
|
||||
clear: function() {
|
||||
var content = Ember.A();
|
||||
content.clear().pushObject(this.makeItem(0, this.get('count') - 1).set('loading', true));
|
||||
this.set('content', content);
|
||||
},
|
||||
|
||||
loadRange: function(start, end, backwards) {
|
||||
var limit = this.get('postLoadCount');
|
||||
loadRange: function(start, end, backwards) {
|
||||
var limit = this.get('postLoadCount');
|
||||
|
||||
// Find the appropriate gap objects in the post stream. When we find
|
||||
// one, we will turn on its loading flag.
|
||||
this.get('content').forEach(function(item) {
|
||||
if (! item.content && ((item.indexStart >= start && item.indexStart <= end) || (item.indexEnd >= start && item.indexEnd <= end))) {
|
||||
item.set('loading', true);
|
||||
item.set('direction', backwards ? 'up' : 'down');
|
||||
}
|
||||
});
|
||||
// Find the appropriate gap objects in the post stream. When we find
|
||||
// one, we will turn on its loading flag.
|
||||
this.get('content').forEach(function(item) {
|
||||
if (!item.content && ((item.indexStart >= start && item.indexStart <= end) || (item.indexEnd >= start && item.indexEnd <= end))) {
|
||||
item.set('loading', true);
|
||||
item.set('direction', backwards ? 'up' : 'down');
|
||||
}
|
||||
});
|
||||
|
||||
// Get a list of post numbers that we'll want to retrieve. If there are
|
||||
// more post IDs than the number of posts we want to load, then take a
|
||||
// slice of the array in the appropriate direction.
|
||||
var ids = this.get('ids').slice(start, end + 1);
|
||||
ids = backwards ? ids.slice(-limit) : ids.slice(0, limit);
|
||||
// Get a list of post numbers that we'll want to retrieve. If there are
|
||||
// more post IDs than the number of posts we want to load, then take a
|
||||
// slice of the array in the appropriate direction.
|
||||
var ids = this.get('ids').slice(start, end + 1);
|
||||
ids = backwards ? ids.slice(-limit) : ids.slice(0, limit);
|
||||
|
||||
return this.loadPosts(ids);
|
||||
},
|
||||
return this.loadPosts(ids);
|
||||
},
|
||||
|
||||
loadPosts: function(ids) {
|
||||
if (! ids.length) {
|
||||
return Ember.RSVP.resolve();
|
||||
}
|
||||
|
||||
var stream = this;
|
||||
return this.store.find('post', {ids: ids}).then(function(posts) {
|
||||
stream.addPosts(posts);
|
||||
});
|
||||
},
|
||||
|
||||
loadNearNumber: function(number) {
|
||||
// Find the item in the post stream which is nearest to this number. If
|
||||
// it turns out the be the actual post we're trying to load, then we can
|
||||
// return a resolved promise (i.e. we don't need to make an API
|
||||
// request.) Or, if it's a gap, we'll switch on its loading flag.
|
||||
var item = this.findNearestToNumber(number);
|
||||
if (item) {
|
||||
if (item.get('content.number') == number) {
|
||||
return Ember.RSVP.resolve([item.get('content')]);
|
||||
} else if (! item.content) {
|
||||
item.set('direction', 'down').set('loading', true);
|
||||
}
|
||||
}
|
||||
|
||||
var stream = this;
|
||||
return this.store.find('post', {
|
||||
discussions: this.get('discussion.id'),
|
||||
near: number,
|
||||
count: this.get('postLoadCount')
|
||||
}).then(function(posts) {
|
||||
stream.addPosts(posts);
|
||||
});
|
||||
},
|
||||
|
||||
loadNearIndex: function(index, backwards) {
|
||||
// Find the item in the post stream which is nearest to this index. If
|
||||
// it turns out the be the actual post we're trying to load, then we can
|
||||
// return a resolved promise (i.e. we don't need to make an API
|
||||
// request.) Or, if it's a gap, we'll switch on its loading flag.
|
||||
var item = this.findNearestToIndex(index);
|
||||
if (item) {
|
||||
if (item.content) {
|
||||
return Ember.RSVP.resolve([item.get('content')]);
|
||||
}
|
||||
return this.loadRange(Math.max(item.indexStart, index - this.get('postLoadCount') / 2), item.indexEnd, backwards);
|
||||
}
|
||||
|
||||
return Ember.RSVP.reject();
|
||||
},
|
||||
|
||||
addPosts: function(posts) {
|
||||
this.trigger('postsLoaded', posts);
|
||||
|
||||
var stream = this;
|
||||
var content = this.get('content');
|
||||
content.beginPropertyChanges();
|
||||
posts.forEach(function(post) {
|
||||
stream.addPost(post);
|
||||
});
|
||||
content.endPropertyChanges();
|
||||
|
||||
this.trigger('postsAdded');
|
||||
},
|
||||
|
||||
addPost: function(post) {
|
||||
var index = this.get('ids').indexOf(post.get('id'));
|
||||
var content = this.get('content');
|
||||
var makeItem = this.makeItem;
|
||||
|
||||
// Here we loop through each item in the post stream, and find the gap
|
||||
// in which this post should be situated. When we find it, we can replace
|
||||
// it with the post, and new gaps either side if appropriate.
|
||||
content.some(function(item, i) {
|
||||
if (item.indexStart <= index && item.indexEnd >= index) {
|
||||
var newItems = [];
|
||||
if (item.indexStart < index) {
|
||||
newItems.push(makeItem(item.indexStart, index - 1));
|
||||
}
|
||||
newItems.push(makeItem(index, index, post));
|
||||
if (item.indexEnd > index) {
|
||||
newItems.push(makeItem(index + 1, item.indexEnd));
|
||||
}
|
||||
content.replace(i, 1, newItems);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addPostToEnd: function(post) {
|
||||
this.get('ids').pushObject(post.get('id'));
|
||||
var index = this.get('count') - 1;
|
||||
this.get('content').pushObject(this.makeItem(index, index, post));
|
||||
},
|
||||
|
||||
makeItem: function(indexStart, indexEnd, post) {
|
||||
var item = Ember.Object.create({
|
||||
indexStart: indexStart,
|
||||
indexEnd: indexEnd
|
||||
});
|
||||
if (post) {
|
||||
item.setProperties({
|
||||
content: post,
|
||||
component: 'discussions/post-'+post.get('type')
|
||||
});
|
||||
}
|
||||
return item;
|
||||
},
|
||||
|
||||
findNearestTo: function(index, property) {
|
||||
var nearestItem;
|
||||
this.get('content').some(function(item) {
|
||||
if (item.get(property) > index) {
|
||||
return true;
|
||||
}
|
||||
nearestItem = item;
|
||||
});
|
||||
return nearestItem;
|
||||
},
|
||||
|
||||
findNearestToNumber: function(number) {
|
||||
return this.findNearestTo(number, 'content.number');
|
||||
},
|
||||
|
||||
findNearestToIndex: function(index) {
|
||||
return this.findNearestTo(index, 'indexStart');
|
||||
loadPosts: function(ids) {
|
||||
if (!ids.length) {
|
||||
return Ember.RSVP.resolve();
|
||||
}
|
||||
|
||||
var stream = this;
|
||||
return this.store.find('post', {ids: ids}).then(function(posts) {
|
||||
stream.addPosts(posts);
|
||||
});
|
||||
},
|
||||
|
||||
loadNearNumber: function(number) {
|
||||
// Find the item in the post stream which is nearest to this number. If
|
||||
// it turns out the be the actual post we're trying to load, then we can
|
||||
// return a resolved promise (i.e. we don't need to make an API
|
||||
// request.) Or, if it's a gap, we'll switch on its loading flag.
|
||||
var item = this.findNearestToNumber(number);
|
||||
if (item) {
|
||||
if (item.get('content.number') == number) {
|
||||
return Ember.RSVP.resolve([item.get('content')]);
|
||||
} else if (! item.content) {
|
||||
item.set('direction', 'down').set('loading', true);
|
||||
}
|
||||
}
|
||||
|
||||
var stream = this;
|
||||
return this.store.find('post', {
|
||||
discussions: this.get('discussion.id'),
|
||||
near: number,
|
||||
count: this.get('postLoadCount')
|
||||
}).then(function(posts) {
|
||||
stream.addPosts(posts);
|
||||
});
|
||||
},
|
||||
|
||||
loadNearIndex: function(index, backwards) {
|
||||
// Find the item in the post stream which is nearest to this index. If
|
||||
// it turns out the be the actual post we're trying to load, then we can
|
||||
// return a resolved promise (i.e. we don't need to make an API
|
||||
// request.) Or, if it's a gap, we'll switch on its loading flag.
|
||||
var item = this.findNearestToIndex(index);
|
||||
if (item) {
|
||||
if (item.content) {
|
||||
return Ember.RSVP.resolve([item.get('content')]);
|
||||
}
|
||||
return this.loadRange(Math.max(item.indexStart, index - this.get('postLoadCount') / 2), item.indexEnd, backwards);
|
||||
}
|
||||
|
||||
return Ember.RSVP.reject();
|
||||
},
|
||||
|
||||
addPosts: function(posts) {
|
||||
this.trigger('postsLoaded', posts);
|
||||
|
||||
var stream = this;
|
||||
var content = this.get('content');
|
||||
content.beginPropertyChanges();
|
||||
posts.forEach(function(post) {
|
||||
stream.addPost(post);
|
||||
});
|
||||
content.endPropertyChanges();
|
||||
|
||||
this.trigger('postsAdded');
|
||||
},
|
||||
|
||||
addPost: function(post) {
|
||||
var index = this.get('ids').indexOf(post.get('id'));
|
||||
var content = this.get('content');
|
||||
var makeItem = this.makeItem;
|
||||
|
||||
// Here we loop through each item in the post stream, and find the gap
|
||||
// in which this post should be situated. When we find it, we can replace
|
||||
// it with the post, and new gaps either side if appropriate.
|
||||
content.some(function(item, i) {
|
||||
if (item.indexStart <= index && item.indexEnd >= index) {
|
||||
var newItems = [];
|
||||
if (item.indexStart < index) {
|
||||
newItems.push(makeItem(item.indexStart, index - 1));
|
||||
}
|
||||
newItems.push(makeItem(index, index, post));
|
||||
if (item.indexEnd > index) {
|
||||
newItems.push(makeItem(index + 1, item.indexEnd));
|
||||
}
|
||||
content.replace(i, 1, newItems);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addPostToEnd: function(post) {
|
||||
this.get('ids').pushObject(post.get('id'));
|
||||
var index = this.get('count') - 1;
|
||||
this.get('content').pushObject(this.makeItem(index, index, post));
|
||||
},
|
||||
|
||||
makeItem: function(indexStart, indexEnd, post) {
|
||||
var item = Ember.Object.create({
|
||||
indexStart: indexStart,
|
||||
indexEnd: indexEnd
|
||||
});
|
||||
if (post) {
|
||||
item.setProperties({
|
||||
content: post,
|
||||
component: 'discussion/post-'+post.get('type')
|
||||
});
|
||||
}
|
||||
return item;
|
||||
},
|
||||
|
||||
findNearestTo: function(index, property) {
|
||||
var nearestItem;
|
||||
this.get('content').some(function(item) {
|
||||
if (item.get(property) > index) {
|
||||
return true;
|
||||
}
|
||||
nearestItem = item;
|
||||
});
|
||||
return nearestItem;
|
||||
},
|
||||
|
||||
findNearestToNumber: function(number) {
|
||||
return this.findNearestTo(number, 'content.number');
|
||||
},
|
||||
|
||||
findNearestToIndex: function(index) {
|
||||
return this.findNearestTo(index, 'indexStart');
|
||||
}
|
||||
});
|
||||
|
@@ -2,25 +2,23 @@ import Ember from 'ember';
|
||||
import DS from 'ember-data';
|
||||
|
||||
export default DS.Model.extend({
|
||||
discussion: DS.belongsTo('discussion', {inverse: 'loadedPosts'}),
|
||||
number: DS.attr('number'),
|
||||
|
||||
discussion: DS.belongsTo('discussion', {inverse: 'loadedPosts'}),
|
||||
number: DS.attr('number'),
|
||||
time: DS.attr('date'),
|
||||
user: DS.belongsTo('user'),
|
||||
type: DS.attr('string'),
|
||||
content: DS.attr('string'),
|
||||
contentHtml: DS.attr('string'),
|
||||
|
||||
time: DS.attr('date'),
|
||||
user: DS.belongsTo('user'),
|
||||
type: DS.attr('string'),
|
||||
content: DS.attr('string'),
|
||||
contentHtml: DS.attr('string'),
|
||||
editTime: DS.attr('date'),
|
||||
editUser: DS.belongsTo('user'),
|
||||
isEdited: Ember.computed.notEmpty('editTime'),
|
||||
|
||||
editTime: DS.attr('date'),
|
||||
editUser: DS.belongsTo('user'),
|
||||
isEdited: Ember.computed.notEmpty('editTime'),
|
||||
|
||||
deleteTime: DS.attr('date'),
|
||||
deleteUser: DS.belongsTo('user'),
|
||||
isDeleted: Ember.computed.notEmpty('deleteTime'),
|
||||
|
||||
canEdit: DS.attr('boolean'),
|
||||
canDelete: DS.attr('boolean')
|
||||
isHidden: DS.attr('boolean'),
|
||||
deleteTime: DS.attr('date'),
|
||||
deleteUser: DS.belongsTo('user'),
|
||||
|
||||
canEdit: DS.attr('boolean'),
|
||||
canDelete: DS.attr('boolean')
|
||||
});
|
||||
|
@@ -1,23 +1,20 @@
|
||||
import DS from 'ember-data';
|
||||
|
||||
export default DS.Model.extend({
|
||||
username: DS.attr('string'),
|
||||
email: DS.attr('string'),
|
||||
password: DS.attr('string'),
|
||||
avatarUrl: DS.attr('string'),
|
||||
|
||||
username: DS.attr('string'),
|
||||
avatarUrl: DS.attr('string'),
|
||||
joinTime: DS.attr('date'),
|
||||
lastSeenTime: DS.attr('date'),
|
||||
discussionsCount: DS.attr('number'),
|
||||
postsCount: DS.attr('number'),
|
||||
groups: DS.hasMany('group'),
|
||||
|
||||
canEdit: DS.attr('boolean'),
|
||||
canDelete: DS.attr('boolean'),
|
||||
joinTime: DS.attr('date'),
|
||||
lastSeenTime: DS.attr('date'),
|
||||
readTime: DS.attr('date'),
|
||||
|
||||
groups: DS.hasMany('group'),
|
||||
discussionsCount: DS.attr('number'),
|
||||
postsCount: DS.attr('number'),
|
||||
|
||||
email: DS.attr('string'),
|
||||
password: DS.attr('string'),
|
||||
|
||||
avatarNumber: function() {
|
||||
return Math.random() > 0.3 ? Math.floor(Math.random() * 19) + 1 : null;
|
||||
}.property()
|
||||
canEdit: DS.attr('boolean'),
|
||||
canDelete: DS.attr('boolean')
|
||||
});
|
||||
|
Reference in New Issue
Block a user