FIX: Highlighting was not being applied after some rendering.

Also includes a bunch of ES6 stuff.
This commit is contained in:
Robin Ward 2015-02-12 15:37:02 -05:00
parent 96697c7957
commit a519fd5bcf
21 changed files with 314 additions and 503 deletions

View File

@ -1,6 +1,7 @@
import ObjectController from 'discourse/controllers/object'; import ObjectController from 'discourse/controllers/object';
import BufferedContent from 'discourse/mixins/buffered-content'; import BufferedContent from 'discourse/mixins/buffered-content';
import { spinnerHTML } from 'discourse/helpers/loading-spinner'; import { spinnerHTML } from 'discourse/helpers/loading-spinner';
import Topic from 'discourse/models/topic';
export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedContent, { export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedContent, {
multiSelect: false, multiSelect: false,
@ -272,7 +273,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
var self = this, var self = this,
props = this.get('buffered.buffer'); props = this.get('buffered.buffer');
Discourse.Topic.update(this.get('model'), props).then(function() { Topic.update(this.get('model'), props).then(function() {
// Note we roll back on success here because `update` saves // Note we roll back on success here because `update` saves
// the properties to the topic. // the properties to the topic.
self.rollbackBuffer(); self.rollbackBuffer();
@ -555,13 +556,13 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
}, },
// Receive notifications for this topic // Receive notifications for this topic
subscribe: function() { subscribe() {
// Unsubscribe before subscribing again // Unsubscribe before subscribing again
this.unsubscribe(); this.unsubscribe();
var topicController = this; const self = this;
Discourse.MessageBus.subscribe("/topic/" + this.get('id'), function(data) { Discourse.MessageBus.subscribe("/topic/" + this.get('id'), function(data) {
var topic = topicController.get('model'); const topic = self.get('model');
if (data.notification_level_change) { if (data.notification_level_change) {
topic.set('details.notification_level', data.notification_level_change); topic.set('details.notification_level', data.notification_level_change);
@ -569,7 +570,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
return; return;
} }
var postStream = topicController.get('postStream'); const postStream = self.get('postStream');
switch (data.type) { switch (data.type) {
case "revised": case "revised":
case "acted": case "acted":

View File

@ -0,0 +1,11 @@
import { decorateCooked } from 'discourse/lib/plugin-api';
import HighlightSyntax from 'discourse/lib/highlight-syntax';
import Lightbox from 'discourse/lib/lightbox';
export default {
name: "post-decorations",
initialize: function(container) {
decorateCooked(container, HighlightSyntax);
decorateCooked(container, Lightbox);
}
};

View File

@ -0,0 +1,10 @@
/*global hljs:true */
export default function highlightSyntax($elem) {
const selector = Discourse.SiteSettings.autohighlight_all_code ? 'pre code' : 'pre code[class]';
$(selector, $elem).each(function(i, e) {
return $LAB.script("/javascripts/highlight.pack.js").wait(function() {
return hljs.highlightBlock(e);
});
});
}

View File

@ -1,54 +0,0 @@
/**
Helper object for lightboxes.
@class Lightbox
@namespace Discourse
@module Discourse
**/
Discourse.Lightbox = {
apply: function($elem) {
$LAB.script("/javascripts/jquery.magnific-popup-min.js").wait(function() {
$("a.lightbox", $elem).each(function(i, e) {
var $e = $(e);
// do not lightbox spoiled images
if ($e.parents(".spoiler").length > 0 || $e.parents(".spoiled").length > 0) { return; }
$e.magnificPopup({
type: "image",
closeOnContentClick: false,
removalDelay: 300,
mainClass: "mfp-zoom-in",
callbacks: {
open: function() {
var wrap = this.wrap,
img = this.currItem.img,
maxHeight = img.css("max-height");
wrap.on("click.pinhandler", "img", function() {
wrap.toggleClass("mfp-force-scrollbars");
img.css("max-height", wrap.hasClass("mfp-force-scrollbars") ? "none" : maxHeight);
});
},
beforeClose: function() {
this.wrap.off("click.pinhandler");
this.wrap.removeClass("mfp-force-scrollbars");
}
},
image: {
titleSrc: function(item) {
var href = item.el.data("download-href") || item.src;
return [
item.el.attr("title"),
$("span.informations", item.el).text().replace('x', '×'),
'<a class="image-source-link" href="' + href + '">' + I18n.t("lightbox.download") + '</a>'
].join(' &middot; ');
}
}
});
});
});
}
};

View File

@ -0,0 +1,45 @@
export default function($elem) {
$("a.lightbox", $elem).each(function(i, e) {
$LAB.script("/javascripts/jquery.magnific-popup-min.js").wait(function() {
var $e = $(e);
// do not lightbox spoiled images
if ($e.parents(".spoiler").length > 0 || $e.parents(".spoiled").length > 0) { return; }
$e.magnificPopup({
type: "image",
closeOnContentClick: false,
removalDelay: 300,
mainClass: "mfp-zoom-in",
callbacks: {
open: function() {
var wrap = this.wrap,
img = this.currItem.img,
maxHeight = img.css("max-height");
wrap.on("click.pinhandler", "img", function() {
wrap.toggleClass("mfp-force-scrollbars");
img.css("max-height", wrap.hasClass("mfp-force-scrollbars") ? "none" : maxHeight);
});
},
beforeClose: function() {
this.wrap.off("click.pinhandler");
this.wrap.removeClass("mfp-force-scrollbars");
}
},
image: {
titleSrc: function(item) {
var href = item.el.data("download-href") || item.src;
return [
item.el.attr("title"),
$("span.informations", item.el).text().replace('x', '&times;'),
'<a class="image-source-link" href="' + href + '">' + I18n.t("lightbox.download") + '</a>'
].join(' &middot; ');
}
}
});
});
});
}

View File

@ -1,4 +1,6 @@
export default function searchForTerm(term, opts) { import Topic from 'discourse/models/topic';
function searchForTerm(term, opts) {
if (!opts) opts = {}; if (!opts) opts = {};
// Only include the data we have // Only include the data we have
@ -22,7 +24,7 @@ export default function searchForTerm(term, opts) {
var topicMap = {}; var topicMap = {};
results.topics = results.topics.map(function(topic){ results.topics = results.topics.map(function(topic){
topic = Discourse.Topic.create(topic); topic = Topic.create(topic);
topicMap[topic.id] = topic; topicMap[topic.id] = topic;
return topic; return topic;
}); });
@ -66,3 +68,5 @@ export default function searchForTerm(term, opts) {
return noResults ? null : Em.Object.create(results); return noResults ? null : Em.Object.create(results);
}); });
} }
export default searchForTerm;

View File

@ -1,26 +0,0 @@
/*global hljs:true */
/**
Helper object for syntax highlighting. Uses highlight.js which is loaded on demand.
@class SyntaxHighlighting
@namespace Discourse
@module Discourse
**/
Discourse.SyntaxHighlighting = {
/**
Apply syntax highlighting to a jQuery element
@method apply
@param {jQuery.selector} $elem The element we want to apply our highlighting to
**/
apply: function($elem) {
var selector = Discourse.SiteSettings.autohighlight_all_code ? 'pre code' : 'pre code[class]';
$(selector, $elem).each(function(i, e) {
return $LAB.script("/javascripts/highlight.pack.js").wait(function() {
return hljs.highlightBlock(e);
});
});
}
};

View File

@ -532,6 +532,7 @@ Discourse.Composer = Discourse.Model.extend({
}); });
} }
// If we're in a topic, we can append the post instantly. // If we're in a topic, we can append the post instantly.
if (postStream) { if (postStream) {
// If it's in reply to another post, increase the reply count // If it's in reply to another post, increase the reply count

View File

@ -1,5 +1,4 @@
Discourse.PostStream = Em.Object.extend({ const PostStream = Ember.Object.extend({
loading: Em.computed.or('loadingAbove', 'loadingBelow', 'loadingFilter', 'stagingPost'), loading: Em.computed.or('loadingAbove', 'loadingBelow', 'loadingFilter', 'stagingPost'),
notLoading: Em.computed.not('loading'), notLoading: Em.computed.not('loading'),
filteredPostsCount: Em.computed.alias("stream.length"), filteredPostsCount: Em.computed.alias("stream.length"),
@ -45,15 +44,13 @@ Discourse.PostStream = Em.Object.extend({
/** /**
Returns a JS Object of current stream filter options. It should match the query Returns a JS Object of current stream filter options. It should match the query
params for the stream. params for the stream.
@property streamFilters
**/ **/
streamFilters: function() { streamFilters: function() {
var result = {}; const result = {};
if (this.get('summary')) { result.filter = "summary"; } if (this.get('summary')) { result.filter = "summary"; }
if (this.get('show_deleted')) { result.show_deleted = true; } if (this.get('show_deleted')) { result.show_deleted = true; }
var userFilters = this.get('userFilters'); const userFilters = this.get('userFilters');
if (!Em.isEmpty(userFilters)) { if (!Em.isEmpty(userFilters)) {
result.username_filters = userFilters.join(","); result.username_filters = userFilters.join(",");
} }
@ -62,27 +59,25 @@ Discourse.PostStream = Em.Object.extend({
}.property('userFilters.[]', 'summary', 'show_deleted'), }.property('userFilters.[]', 'summary', 'show_deleted'),
hasNoFilters: function() { hasNoFilters: function() {
var streamFilters = this.get('streamFilters'); const streamFilters = this.get('streamFilters');
return !(streamFilters && ((streamFilters.filter === 'summary') || streamFilters.username_filters)); return !(streamFilters && ((streamFilters.filter === 'summary') || streamFilters.username_filters));
}.property('streamFilters.[]', 'topic.posts_count', 'posts.length'), }.property('streamFilters.[]', 'topic.posts_count', 'posts.length'),
/** /**
Returns the window of posts above the current set in the stream, bound to the top of the stream. Returns the window of posts above the current set in the stream, bound to the top of the stream.
This is the collection we'll ask for when scrolling upwards. This is the collection we'll ask for when scrolling upwards.
@property previousWindow
**/ **/
previousWindow: function() { previousWindow: function() {
// If we can't find the last post loaded, bail // If we can't find the last post loaded, bail
var firstPost = _.first(this.get('posts')); const firstPost = _.first(this.get('posts'));
if (!firstPost) { return []; } if (!firstPost) { return []; }
// Find the index of the last post loaded, if not found, bail // Find the index of the last post loaded, if not found, bail
var stream = this.get('stream'); const stream = this.get('stream');
var firstIndex = this.indexOf(firstPost); const firstIndex = this.indexOf(firstPost);
if (firstIndex === -1) { return []; } if (firstIndex === -1) { return []; }
var startIndex = firstIndex - this.get('topic.chunk_size'); let startIndex = firstIndex - this.get('topic.chunk_size');
if (startIndex < 0) { startIndex = 0; } if (startIndex < 0) { startIndex = 0; }
return stream.slice(startIndex, firstIndex); return stream.slice(startIndex, firstIndex);
@ -91,17 +86,15 @@ Discourse.PostStream = Em.Object.extend({
/** /**
Returns the window of posts below the current set in the stream, bound by the bottom of the Returns the window of posts below the current set in the stream, bound by the bottom of the
stream. This is the collection we use when scrolling downwards. stream. This is the collection we use when scrolling downwards.
@property nextWindow
**/ **/
nextWindow: function() { nextWindow: function() {
// If we can't find the last post loaded, bail // If we can't find the last post loaded, bail
var lastLoadedPost = this.get('lastLoadedPost'); const lastLoadedPost = this.get('lastLoadedPost');
if (!lastLoadedPost) { return []; } if (!lastLoadedPost) { return []; }
// Find the index of the last post loaded, if not found, bail // Find the index of the last post loaded, if not found, bail
var stream = this.get('stream'); const stream = this.get('stream');
var lastIndex = this.indexOf(lastLoadedPost); const lastIndex = this.indexOf(lastLoadedPost);
if (lastIndex === -1) { return []; } if (lastIndex === -1) { return []; }
if ((lastIndex + 1) >= this.get('highest_post_number')) { return []; } if ((lastIndex + 1) >= this.get('highest_post_number')) { return []; }
@ -109,41 +102,26 @@ Discourse.PostStream = Em.Object.extend({
return stream.slice(lastIndex+1, lastIndex + this.get('topic.chunk_size') + 1); return stream.slice(lastIndex+1, lastIndex + this.get('topic.chunk_size') + 1);
}.property('lastLoadedPost', 'stream.@each'), }.property('lastLoadedPost', 'stream.@each'),
cancelFilter() {
/**
Cancel any active filters on the stream.
@method cancelFilter
**/
cancelFilter: function() {
this.set('summary', false); this.set('summary', false);
this.set('show_deleted', false); this.set('show_deleted', false);
this.get('userFilters').clear(); this.get('userFilters').clear();
}, },
/** toggleSummary() {
Toggle summary mode for the stream.
@method toggleSummary
**/
toggleSummary: function() {
this.get('userFilters').clear(); this.get('userFilters').clear();
this.toggleProperty('summary'); this.toggleProperty('summary');
return this.refresh(); return this.refresh();
}, },
toggleDeleted: function() { toggleDeleted() {
this.toggleProperty('show_deleted'); this.toggleProperty('show_deleted');
return this.refresh(); return this.refresh();
}, },
/** // Filter the stream to a particular user.
Filter the stream to a particular user. toggleParticipant(username) {
const userFilters = this.get('userFilters');
@method toggleParticipant
**/
toggleParticipant: function(username) {
var userFilters = this.get('userFilters');
this.set('summary', false); this.set('summary', false);
this.set('show_deleted', true); this.set('show_deleted', true);
if (userFilters.contains(username)) { if (userFilters.contains(username)) {
@ -157,22 +135,16 @@ Discourse.PostStream = Em.Object.extend({
/** /**
Loads a new set of posts into the stream. If you provide a `nearPost` option and the post Loads a new set of posts into the stream. If you provide a `nearPost` option and the post
is already loaded, it will simply scroll there and load nothing. is already loaded, it will simply scroll there and load nothing.
@method refresh
@param {Object} opts Options for loading the stream
@param {Integer} opts.nearPost The post we want to find other posts near to.
@param {Boolean} opts.track_visit Whether or not to track this as a visit to a topic.
@returns {Promise} a promise that is resolved when the posts have been inserted into the stream.
**/ **/
refresh: function(opts) { refresh(opts) {
opts = opts || {}; opts = opts || {};
opts.nearPost = parseInt(opts.nearPost, 10); opts.nearPost = parseInt(opts.nearPost, 10);
var topic = this.get('topic'), const topic = this.get('topic'),
self = this; self = this;
// Do we already have the post in our list of posts? Jump there. // Do we already have the post in our list of posts? Jump there.
var postWeWant = this.get('posts').findProperty('post_number', opts.nearPost); const postWeWant = this.get('posts').findProperty('post_number', opts.nearPost);
if (postWeWant) { return Ember.RSVP.resolve(); } if (postWeWant) { return Ember.RSVP.resolve(); }
// TODO: if we have all the posts in the filter, don't go to the server for them. // TODO: if we have all the posts in the filter, don't go to the server for them.
@ -192,10 +164,10 @@ Discourse.PostStream = Em.Object.extend({
}, },
hasLoadedData: Em.computed.and('hasPosts', 'hasStream'), hasLoadedData: Em.computed.and('hasPosts', 'hasStream'),
collapsePosts: function(from, to){ collapsePosts(from, to){
var posts = this.get('posts'); const posts = this.get('posts');
var remove = posts.filter(function(post){ const remove = posts.filter(function(post){
var postNumber = post.get('post_number'); const postNumber = post.get('post_number');
return postNumber >= from && postNumber <= to; return postNumber >= from && postNumber <= to;
}); });
@ -203,9 +175,9 @@ Discourse.PostStream = Em.Object.extend({
// make gap // make gap
this.set('gaps', this.get('gaps') || {before: {}, after: {}}); this.set('gaps', this.get('gaps') || {before: {}, after: {}});
var before = this.get('gaps.before'); const before = this.get('gaps.before');
var post = posts.find(function(post){ const post = posts.find(function(post){
return post.get('post_number') > to; return post.get('post_number') > to;
}); });
@ -218,16 +190,9 @@ Discourse.PostStream = Em.Object.extend({
}, },
/** // Fill in a gap of posts before a particular post
Fill in a gap of posts before a particular post fillGapBefore(post, gap) {
const postId = post.get('id'),
@method fillGapBefore
@paaram {Discourse.Post} post beside gap
@paaram {Array} gap array of post ids to load
@returns {Promise} a promise that's resolved when the posts have been added.
**/
fillGapBefore: function(post, gap) {
var postId = post.get('id'),
stream = this.get('stream'), stream = this.get('stream'),
idx = stream.indexOf(postId), idx = stream.indexOf(postId),
currentPosts = this.get('posts'), currentPosts = this.get('posts'),
@ -237,11 +202,11 @@ Discourse.PostStream = Em.Object.extend({
// Insert the gap at the appropriate place // Insert the gap at the appropriate place
stream.splice.apply(stream, [idx, 0].concat(gap)); stream.splice.apply(stream, [idx, 0].concat(gap));
var postIdx = currentPosts.indexOf(post); let postIdx = currentPosts.indexOf(post);
if (postIdx !== -1) { if (postIdx !== -1) {
return this.findPostsByIds(gap).then(function(posts) { return this.findPostsByIds(gap).then(function(posts) {
posts.forEach(function(p) { posts.forEach(function(p) {
var stored = self.storePost(p); const stored = self.storePost(p);
if (!currentPosts.contains(stored)) { if (!currentPosts.contains(stored)) {
currentPosts.insertAt(postIdx++, stored); currentPosts.insertAt(postIdx++, stored);
} }
@ -256,16 +221,9 @@ Discourse.PostStream = Em.Object.extend({
return Ember.RSVP.resolve(); return Ember.RSVP.resolve();
}, },
/** // Fill in a gap of posts after a particular post
Fill in a gap of posts after a particular post fillGapAfter(post, gap) {
const postId = post.get('id'),
@method fillGapAfter
@paaram {Discourse.Post} post beside gap
@paaram {Array} gap array of post ids to load
@returns {Promise} a promise that's resolved when the posts have been added.
**/
fillGapAfter: function(post, gap) {
var postId = post.get('id'),
stream = this.get('stream'), stream = this.get('stream'),
idx = stream.indexOf(postId), idx = stream.indexOf(postId),
self = this; self = this;
@ -279,24 +237,19 @@ Discourse.PostStream = Em.Object.extend({
return Ember.RSVP.resolve(); return Ember.RSVP.resolve();
}, },
/** // Appends the next window of posts to the stream. Call it when scrolling downwards.
Appends the next window of posts to the stream. Call it when scrolling downwards. appendMore() {
const self = this;
@method appendMore
@returns {Promise} a promise that's resolved when the posts have been added.
**/
appendMore: function() {
var self = this;
// Make sure we can append more posts // Make sure we can append more posts
if (!self.get('canAppendMore')) { return Ember.RSVP.resolve(); } if (!self.get('canAppendMore')) { return Ember.RSVP.resolve(); }
var postIds = self.get('nextWindow'); const postIds = self.get('nextWindow');
if (Ember.isEmpty(postIds)) { return Ember.RSVP.resolve(); } if (Ember.isEmpty(postIds)) { return Ember.RSVP.resolve(); }
self.set('loadingBelow', true); self.set('loadingBelow', true);
var stopLoading = function() { const stopLoading = function() {
self.set('loadingBelow', false); self.set('loadingBelow', false);
}; };
@ -308,19 +261,14 @@ Discourse.PostStream = Em.Object.extend({
}, stopLoading); }, stopLoading);
}, },
/** // Prepend the previous window of posts to the stream. Call it when scrolling upwards.
Prepend the previous window of posts to the stream. Call it when scrolling upwards. prependMore() {
const postStream = this;
@method prependMore
@returns {Promise} a promise that's resolved when the posts have been added.
**/
prependMore: function() {
var postStream = this;
// Make sure we can append more posts // Make sure we can append more posts
if (!postStream.get('canPrependMore')) { return Ember.RSVP.resolve(); } if (!postStream.get('canPrependMore')) { return Ember.RSVP.resolve(); }
var postIds = postStream.get('previousWindow'); const postIds = postStream.get('previousWindow');
if (Ember.isEmpty(postIds)) { return Ember.RSVP.resolve(); } if (Ember.isEmpty(postIds)) { return Ember.RSVP.resolve(); }
postStream.set('loadingAbove', true); postStream.set('loadingAbove', true);
@ -336,18 +284,13 @@ Discourse.PostStream = Em.Object.extend({
Stage a post for insertion in the stream. It should be rendered right away under the Stage a post for insertion in the stream. It should be rendered right away under the
assumption that the post will succeed. We can then `commitPost` when it succeeds or assumption that the post will succeed. We can then `commitPost` when it succeeds or
`undoPost` when it fails. `undoPost` when it fails.
@method stagePost
@param {Discourse.Post} post the post to stage in the stream
@param {Discourse.User} user the user creating the post
**/ **/
stagePost: function(post, user) { stagePost(post, user) {
// We can't stage two posts simultaneously // We can't stage two posts simultaneously
if (this.get('stagingPost')) { return false; } if (this.get('stagingPost')) { return false; }
this.set('stagingPost', true); this.set('stagingPost', true);
var topic = this.get('topic'); const topic = this.get('topic');
topic.setProperties({ topic.setProperties({
posts_count: (topic.get('posts_count') || 0) + 1, posts_count: (topic.get('posts_count') || 0) + 1,
last_posted_at: new Date(), last_posted_at: new Date(),
@ -371,13 +314,8 @@ Discourse.PostStream = Em.Object.extend({
return true; return true;
}, },
/** // Commit the post we staged. Call this after a save succeeds.
Commit the post we staged. Call this after a save succeeds. commitPost(post) {
@method commitPost
@param {Discourse.Post} the post we saved in the stream.
**/
commitPost: function(post) {
if (this.get('loadedAllPosts')) { if (this.get('loadedAllPosts')) {
this.appendPost(post); this.appendPost(post);
} }
@ -398,16 +336,13 @@ Discourse.PostStream = Em.Object.extend({
/** /**
Undo a post we've staged in the stream. Remove it from being rendered and revert the Undo a post we've staged in the stream. Remove it from being rendered and revert the
state we changed. state we changed.
@method undoPost
@param {Discourse.Post} the post to undo from the stream
**/ **/
undoPost: function(post) { undoPost(post) {
this.get('stream').removeObject(-1); this.get('stream').removeObject(-1);
this.posts.removeObject(post); this.posts.removeObject(post);
this.get('postIdentityMap').set(-1, null); this.get('postIdentityMap').set(-1, null);
var topic = this.get('topic'); const topic = this.get('topic');
this.set('stagingPost', false); this.set('stagingPost', false);
topic.setProperties({ topic.setProperties({
@ -418,44 +353,24 @@ Discourse.PostStream = Em.Object.extend({
// TODO unfudge reply count on parent post // TODO unfudge reply count on parent post
}, },
/** prependPost(post) {
Prepends a single post to the stream.
@method prependPost
@param {Discourse.Post} post The post we're prepending
@returns {Discourse.Post} the post that was inserted.
**/
prependPost: function(post) {
this.get('posts').unshiftObject(this.storePost(post)); this.get('posts').unshiftObject(this.storePost(post));
return post; return post;
}, },
/** appendPost(post) {
Appends a single post into the stream. const stored = this.storePost(post);
@method appendPost
@param {Discourse.Post} post The post we're appending
@returns {Discourse.Post} the post that was inserted.
**/
appendPost: function(post) {
var stored = this.storePost(post);
if (stored) { if (stored) {
this.get('posts').addObject(stored); this.get('posts').addObject(stored);
} }
return post; return post;
}, },
/** removePosts(posts) {
Removes posts from the stream.
@method removePosts
@param {Array} posts the collection of posts to remove
**/
removePosts: function(posts) {
if (Em.isEmpty(posts)) { return; } if (Em.isEmpty(posts)) { return; }
var postIds = posts.map(function (p) { return p.get('id'); }); const postIds = posts.map(function (p) { return p.get('id'); });
var identityMap = this.get('postIdentityMap'); const identityMap = this.get('postIdentityMap');
this.get('stream').removeObjects(postIds); this.get('stream').removeObjects(postIds);
this.get('posts').removeObjects(posts); this.get('posts').removeObjects(posts);
@ -464,14 +379,8 @@ Discourse.PostStream = Em.Object.extend({
}); });
}, },
/** // Returns a post from the identity map if it's been inserted.
Returns a post from the identity map if it's been inserted. findLoadedPost(id) {
@method findLoadedPost
@param {Integer} id The post we want from the identity map.
@returns {Discourse.Post} the post that was inserted.
**/
findLoadedPost: function(id) {
return this.get('postIdentityMap').get(id); return this.get('postIdentityMap').get(id);
}, },
@ -479,17 +388,14 @@ Discourse.PostStream = Em.Object.extend({
Finds and adds a post to the stream by id. Typically this would happen if we receive a message Finds and adds a post to the stream by id. Typically this would happen if we receive a message
from the message bus indicating there's a new post. We'll only insert it if we currently from the message bus indicating there's a new post. We'll only insert it if we currently
have no filters. have no filters.
@method triggerNewPostInStream
@param {Integer} postId The id of the new post to be inserted into the stream
**/ **/
triggerNewPostInStream: function(postId) { triggerNewPostInStream(postId) {
if (!postId) { return; } if (!postId) { return; }
// We only trigger if there are no filters active // We only trigger if there are no filters active
if (!this.get('hasNoFilters')) { return; } if (!this.get('hasNoFilters')) { return; }
var loadedAllPosts = this.get('loadedAllPosts'); const loadedAllPosts = this.get('loadedAllPosts');
if (this.get('stream').indexOf(postId) === -1) { if (this.get('stream').indexOf(postId) === -1) {
this.get('stream').addObject(postId); this.get('stream').addObject(postId);
@ -497,8 +403,8 @@ Discourse.PostStream = Em.Object.extend({
} }
}, },
triggerRecoveredPost: function(postId){ triggerRecoveredPost(postId){
var self = this, const self = this,
postIdentityMap = this.get('postIdentityMap'), postIdentityMap = this.get('postIdentityMap'),
existing = postIdentityMap.get(postId); existing = postIdentityMap.get(postId);
@ -506,15 +412,15 @@ Discourse.PostStream = Em.Object.extend({
this.triggerChangedPost(postId, new Date()); this.triggerChangedPost(postId, new Date());
} else { } else {
// need to insert into stream // need to insert into stream
var url = "/posts/" + postId; const url = "/posts/" + postId;
Discourse.ajax(url).then(function(p){ Discourse.ajax(url).then(function(p){
var post = Discourse.Post.create(p); const post = Discourse.Post.create(p);
var stream = self.get("stream"); const stream = self.get("stream");
var posts = self.get("posts"); const posts = self.get("posts");
self.storePost(post); self.storePost(post);
// we need to zip this into the stream // we need to zip this into the stream
var index = 0; let index = 0;
stream.forEach(function(postId){ stream.forEach(function(postId){
if(postId < p.id){ if(postId < p.id){
index+= 1; index+= 1;
@ -541,13 +447,13 @@ Discourse.PostStream = Em.Object.extend({
} }
}, },
triggerDeletedPost: function(postId){ triggerDeletedPost(postId){
var self = this, const self = this,
postIdentityMap = this.get('postIdentityMap'), postIdentityMap = this.get('postIdentityMap'),
existing = postIdentityMap.get(postId); existing = postIdentityMap.get(postId);
if(existing){ if(existing){
var url = "/posts/" + postId; const url = "/posts/" + postId;
Discourse.ajax(url).then( Discourse.ajax(url).then(
function(p){ function(p){
self.storePost(Discourse.Post.create(p)); self.storePost(Discourse.Post.create(p));
@ -558,30 +464,24 @@ Discourse.PostStream = Em.Object.extend({
} }
}, },
triggerChangedPost: function(postId, updatedAt) { triggerChangedPost(postId, updatedAt) {
if (!postId) { return; } if (!postId) { return; }
var postIdentityMap = this.get('postIdentityMap'), const postIdentityMap = this.get('postIdentityMap'),
existing = postIdentityMap.get(postId), existing = postIdentityMap.get(postId),
self = this; self = this;
if (existing && existing.updated_at !== updatedAt) { if (existing && existing.updated_at !== updatedAt) {
var url = "/posts/" + postId; const url = "/posts/" + postId;
Discourse.ajax(url).then(function(p){ Discourse.ajax(url).then(function(p){
self.storePost(Discourse.Post.create(p)); self.storePost(Discourse.Post.create(p));
}); });
} }
}, },
/** // Returns the "thread" of posts in the history of a post.
Returns the "thread" of posts in the history of a post. findReplyHistory(post) {
const postStream = this,
@method findReplyHistory
@param {Discourse.Post} post the post whose history we want
@returns {Array} the posts in the history.
**/
findReplyHistory: function(post) {
var postStream = this,
url = "/posts/" + post.get('id') + "/reply-history.json?max_replies=" + Discourse.SiteSettings.max_reply_history; url = "/posts/" + post.get('id') + "/reply-history.json?max_replies=" + Discourse.SiteSettings.max_reply_history;
return Discourse.ajax(url).then(function(result) { return Discourse.ajax(url).then(function(result) {
@ -597,16 +497,11 @@ Discourse.PostStream = Em.Object.extend({
Returns the closest post given a postNumber that may not exist in the stream. Returns the closest post given a postNumber that may not exist in the stream.
For example, if the user asks for a post that's deleted or otherwise outside the range. For example, if the user asks for a post that's deleted or otherwise outside the range.
This allows us to set the progress bar with the correct number. This allows us to set the progress bar with the correct number.
@method closestPostForPostNumber
@param {Number} postNumber the post number we're looking for
@return {Post} the closest post
@see PostStream.closestPostNumberFor
**/ **/
closestPostForPostNumber: function(postNumber) { closestPostForPostNumber(postNumber) {
if (!this.get('hasPosts')) { return; } if (!this.get('hasPosts')) { return; }
var closest = null; let closest = null;
this.get('posts').forEach(function (p) { this.get('posts').forEach(function (p) {
if (!closest) { if (!closest) {
closest = p; closest = p;
@ -628,17 +523,12 @@ Discourse.PostStream = Em.Object.extend({
@returns {Number} 1-starting index of the post, or 0 if not found @returns {Number} 1-starting index of the post, or 0 if not found
@see PostStream.progressIndexOfPostId @see PostStream.progressIndexOfPostId
**/ **/
progressIndexOfPost: function(post) { progressIndexOfPost(post) {
return this.progressIndexOfPostId(post.get('id')); return this.progressIndexOfPostId(post.get('id'));
}, },
/** // Get the index in the stream of a post id. (Use this for the topic progress bar.)
Get the index in the stream of a post id. (Use this for the topic progress bar.) progressIndexOfPostId(post_id) {
@param post_id - post id to search for
@returns {Number} 1-starting index of the post, or 0 if not found
**/
progressIndexOfPostId: function(post_id) {
return this.get('stream').indexOf(post_id) + 1; return this.get('stream').indexOf(post_id) + 1;
}, },
@ -646,15 +536,11 @@ Discourse.PostStream = Em.Object.extend({
Returns the closest post number given a postNumber that may not exist in the stream. Returns the closest post number given a postNumber that may not exist in the stream.
For example, if the user asks for a post that's deleted or otherwise outside the range. For example, if the user asks for a post that's deleted or otherwise outside the range.
This allows us to set the progress bar with the correct number. This allows us to set the progress bar with the correct number.
@method closestPostNumberFor
@param {Number} postNumber the post number we're looking for
@return {Number} a close post number
**/ **/
closestPostNumberFor: function(postNumber) { closestPostNumberFor(postNumber) {
if (!this.get('hasPosts')) { return; } if (!this.get('hasPosts')) { return; }
var closest = null; let closest = null;
this.get('posts').forEach(function (p) { this.get('posts').forEach(function (p) {
if (closest === postNumber) { return; } if (closest === postNumber) { return; }
if (!closest) { closest = p.get('post_number'); } if (!closest) { closest = p.get('post_number'); }
@ -668,41 +554,33 @@ Discourse.PostStream = Em.Object.extend({
}, },
// Find a postId for a postNumber, respecting gaps // Find a postId for a postNumber, respecting gaps
findPostIdForPostNumber: function(postNumber) { findPostIdForPostNumber(postNumber) {
var count = 1, const stream = this.get('stream'),
stream = this.get('stream'),
beforeLookup = this.get('gaps.before'), beforeLookup = this.get('gaps.before'),
streamLength = stream.length; streamLength = stream.length;
for (var i=0; i<streamLength; i++) { let sum = 1;
var pid = stream[i]; for (let i=0; i<streamLength; i++) {
const pid = stream[i];
// See if there are posts before this post // See if there are posts before this post
if (beforeLookup) { if (beforeLookup) {
var before = beforeLookup[pid]; const before = beforeLookup[pid];
if (before) { if (before) {
for (var j=0; j<before.length; j++) { for (let j=0; j<before.length; j++) {
if (count === postNumber) { return pid; } if (sum === postNumber) { return pid; }
count++; sum++;
} }
} }
} }
if (count === postNumber) { return pid; } if (sum === postNumber) { return pid; }
count++; sum++;
} }
}, },
/** updateFromJson(postStreamData) {
@private const postStream = this,
Given a JSON packet, update this stream and the posts that exist in it.
@param {Object} postStreamData The JSON data we want to update from.
@method updateFromJson
**/
updateFromJson: function(postStreamData) {
var postStream = this,
posts = this.get('posts'); posts = this.get('posts');
posts.clear(); posts.clear();
@ -720,23 +598,17 @@ Discourse.PostStream = Em.Object.extend({
}, },
/** /**
@private
Stores a post in our identity map, and sets up the references it needs to Stores a post in our identity map, and sets up the references it needs to
find associated objects like the topic. It might return a different reference find associated objects like the topic. It might return a different reference
than you supplied if the post has already been loaded. than you supplied if the post has already been loaded.
@method storePost
@param {Discourse.Post} post The post we're storing in the identity map
@returns {Discourse.Post} the post from the identity map
**/ **/
storePost: function(post) { storePost(post) {
// Calling `Em.get(undefined` raises an error // Calling `Em.get(undefined` raises an error
if (!post) { return; } if (!post) { return; }
var postId = Em.get(post, 'id'); const postId = Em.get(post, 'id');
if (postId) { if (postId) {
var postIdentityMap = this.get('postIdentityMap'), const postIdentityMap = this.get('postIdentityMap'),
existing = postIdentityMap.get(post.get('id')); existing = postIdentityMap.get(post.get('id'));
if (existing) { if (existing) {
@ -752,7 +624,7 @@ Discourse.PostStream = Em.Object.extend({
postIdentityMap.set(post.get('id'), post); postIdentityMap.set(post.get('id'), post);
// Update the `highest_post_number` if this post is higher. // Update the `highest_post_number` if this post is higher.
var postNumber = post.get('post_number'); const postNumber = post.get('post_number');
if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) { if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) {
this.set('topic.highest_post_number', postNumber); this.set('topic.highest_post_number', postNumber);
} }
@ -761,17 +633,11 @@ Discourse.PostStream = Em.Object.extend({
}, },
/** /**
@private
Given a list of postIds, returns a list of the posts we don't have in our Given a list of postIds, returns a list of the posts we don't have in our
identity map and need to load. identity map and need to load.
@method listUnloadedIds
@param {Array} postIds The post Ids we want to load from the server
@returns {Array} the array of postIds we don't have loaded.
**/ **/
listUnloadedIds: function(postIds) { listUnloadedIds(postIds) {
var unloaded = Em.A(), const unloaded = Em.A(),
postIdentityMap = this.get('postIdentityMap'); postIdentityMap = this.get('postIdentityMap');
postIds.forEach(function(p) { postIds.forEach(function(p) {
if (!postIdentityMap.has(p)) { unloaded.pushObject(p); } if (!postIdentityMap.has(p)) { unloaded.pushObject(p); }
@ -779,17 +645,8 @@ Discourse.PostStream = Em.Object.extend({
return unloaded; return unloaded;
}, },
/** findPostsByIds(postIds) {
@private const unloaded = this.listUnloadedIds(postIds),
Returns a list of posts in order requested, by id.
@method findPostsByIds
@param {Array} postIds The post Ids we want to retrieve, in order.
@returns {Promise} a promise that will resolve to the posts in the order requested.
**/
findPostsByIds: function(postIds) {
var unloaded = this.listUnloadedIds(postIds),
postIdentityMap = this.get('postIdentityMap'); postIdentityMap = this.get('postIdentityMap');
// Load our unloaded posts by id // Load our unloaded posts by id
@ -800,27 +657,18 @@ Discourse.PostStream = Em.Object.extend({
}); });
}, },
/** loadIntoIdentityMap(postIds) {
@private
Loads a list of posts from the server and inserts them into our identity map.
@method loadIntoIdentityMap
@param {Array} postIds The post Ids we want to insert into the identity map.
@returns {Promise} a promise that will resolve to the posts in the order requested.
**/
loadIntoIdentityMap: function(postIds) {
// If we don't want any posts, return a promise that resolves right away // If we don't want any posts, return a promise that resolves right away
if (Em.isEmpty(postIds)) { if (Em.isEmpty(postIds)) {
return Ember.RSVP.resolve(); return Ember.RSVP.resolve();
} }
var url = "/t/" + this.get('topic.id') + "/posts.json", const url = "/t/" + this.get('topic.id') + "/posts.json",
data = { post_ids: postIds }, data = { post_ids: postIds },
postStream = this; postStream = this;
return Discourse.ajax(url, {data: data}).then(function(result) { return Discourse.ajax(url, {data: data}).then(function(result) {
var posts = Em.get(result, "post_stream.posts"); const posts = Em.get(result, "post_stream.posts");
if (posts) { if (posts) {
posts.forEach(function (p) { posts.forEach(function (p) {
postStream.storePost(Discourse.Post.create(p)); postStream.storePost(Discourse.Post.create(p));
@ -830,33 +678,19 @@ Discourse.PostStream = Em.Object.extend({
}, },
/** indexOf(post) {
@private
Returns the index of a particular post in the stream
@method indexOf
@param {Discourse.Post} post The post we're looking for
**/
indexOf: function(post) {
return this.get('stream').indexOf(post.get('id')); return this.get('stream').indexOf(post.get('id'));
}, },
/** /**
@private
Handles an error loading a topic based on a HTTP status code. Updates Handles an error loading a topic based on a HTTP status code. Updates
the text to the correct values. the text to the correct values.
@method errorLoading
@param {Integer} status the HTTP status code
@param {Discourse.Topic} topic The topic instance we were trying to load
**/ **/
errorLoading: function(result) { errorLoading(result) {
var status = result.status; const status = result.status;
var topic = this.get('topic'); const topic = this.get('topic');
topic.set('loadingFilter', false); topic.set('loadingFilter', false);
topic.set('errorLoading', true); topic.set('errorLoading', true);
@ -887,10 +721,10 @@ Discourse.PostStream = Em.Object.extend({
}); });
Discourse.PostStream.reopenClass({ PostStream.reopenClass({
create: function() { create() {
var postStream = this._super.apply(this, arguments); const postStream = this._super.apply(this, arguments);
postStream.setProperties({ postStream.setProperties({
posts: [], posts: [],
stream: [], stream: [],
@ -906,9 +740,9 @@ Discourse.PostStream.reopenClass({
return postStream; return postStream;
}, },
loadTopicView: function(topicId, args) { loadTopicView(topicId, args) {
var opts = _.merge({}, args), const opts = _.merge({}, args);
url = Discourse.getURL("/t/") + topicId; let url = Discourse.getURL("/t/") + topicId;
if (opts.nearPost) { if (opts.nearPost) {
url += "/" + opts.nearPost; url += "/" + opts.nearPost;
} }
@ -921,3 +755,5 @@ Discourse.PostStream.reopenClass({
} }
}); });
export default PostStream;

View File

@ -1,16 +1,13 @@
/** /**
A model representing a Topic's details that aren't always present, such as a list of participants. A model representing a Topic's details that aren't always present, such as a list of participants.
When showing topics in lists and such this information should not be required. When showing topics in lists and such this information should not be required.
@class TopicDetails
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/ **/
Discourse.TopicDetails = Discourse.Model.extend({ const TopicDetails = Discourse.Model.extend({
loaded: false, loaded: false,
updateFromJson: function(details) { updateFromJson(details) {
const topic = this.get('topic');
if (details.allowed_users) { if (details.allowed_users) {
details.allowed_users = details.allowed_users.map(function (u) { details.allowed_users = details.allowed_users.map(function (u) {
return Discourse.User.create(u); return Discourse.User.create(u);
@ -24,10 +21,9 @@ Discourse.TopicDetails = Discourse.Model.extend({
} }
if (details.participants) { if (details.participants) {
var topic = this.get('topic');
details.participants = details.participants.map(function (p) { details.participants = details.participants.map(function (p) {
p.topic = topic; p.topic = topic;
return Em.Object.create(p); return Ember.Object.create(p);
}); });
} }
@ -59,7 +55,7 @@ Discourse.TopicDetails = Discourse.Model.extend({
}.property('notification_level', 'notifications_reason_id'), }.property('notification_level', 'notifications_reason_id'),
updateNotifications: function(v) { updateNotifications(v) {
this.set('notification_level', v); this.set('notification_level', v);
this.set('notifications_reason_id', null); this.set('notifications_reason_id', null);
return Discourse.ajax("/t/" + (this.get('topic.id')) + "/notifications", { return Discourse.ajax("/t/" + (this.get('topic.id')) + "/notifications", {
@ -68,7 +64,7 @@ Discourse.TopicDetails = Discourse.Model.extend({
}); });
}, },
removeAllowedUser: function(user) { removeAllowedUser(user) {
var users = this.get('allowed_users'), var users = this.get('allowed_users'),
username = user.get('username'); username = user.get('username');
@ -80,3 +76,5 @@ Discourse.TopicDetails = Discourse.Model.extend({
}); });
} }
}); });
export default TopicDetails;

View File

@ -1,8 +1,11 @@
Discourse.Topic = Discourse.Model.extend({ import TopicDetails from 'discourse/models/topic-details';
import PostStream from 'discourse/models/post-stream';
const Topic = Discourse.Model.extend({
// returns createdAt if there's no bumped date // returns createdAt if there's no bumped date
bumpedAt: function() { bumpedAt: function() {
var bumpedAt = this.get('bumped_at'); const bumpedAt = this.get('bumped_at');
if (bumpedAt) { if (bumpedAt) {
return new Date(bumpedAt); return new Date(bumpedAt);
} else { } else {
@ -20,11 +23,11 @@ Discourse.Topic = Discourse.Model.extend({
}.property('created_at'), }.property('created_at'),
postStream: function() { postStream: function() {
return Discourse.PostStream.create({topic: this}); return PostStream.create({topic: this});
}.property(), }.property(),
details: function() { details: function() {
return Discourse.TopicDetails.create({topic: this}); return TopicDetails.create({topic: this});
}.property(), }.property(),
invisible: Em.computed.not('visible'), invisible: Em.computed.not('visible'),
@ -35,12 +38,12 @@ Discourse.Topic = Discourse.Model.extend({
}.property('id'), }.property('id'),
category: function() { category: function() {
var categoryId = this.get('category_id'); const categoryId = this.get('category_id');
if (categoryId) { if (categoryId) {
return Discourse.Category.list().findProperty('id', categoryId); return Discourse.Category.list().findProperty('id', categoryId);
} }
var categoryName = this.get('categoryName'); const categoryName = this.get('categoryName');
if (categoryName) { if (categoryName) {
return Discourse.Category.list().findProperty('name', categoryName); return Discourse.Category.list().findProperty('name', categoryName);
} }
@ -52,12 +55,12 @@ Discourse.Topic = Discourse.Model.extend({
}.property('category.fullSlug'), }.property('category.fullSlug'),
shareUrl: function(){ shareUrl: function(){
var user = Discourse.User.current(); const user = Discourse.User.current();
return this.get('url') + (user ? '?u=' + user.get('username_lower') : ''); return this.get('url') + (user ? '?u=' + user.get('username_lower') : '');
}.property('url'), }.property('url'),
url: function() { url: function() {
var slug = this.get('slug'); let slug = this.get('slug');
if (slug.trim().length === 0) { if (slug.trim().length === 0) {
slug = "topic"; slug = "topic";
} }
@ -65,8 +68,8 @@ Discourse.Topic = Discourse.Model.extend({
}.property('id', 'slug'), }.property('id', 'slug'),
// Helper to build a Url with a post number // Helper to build a Url with a post number
urlForPostNumber: function(postNumber) { urlForPostNumber(postNumber) {
var url = this.get('url'); let url = this.get('url');
if (postNumber && (postNumber > 0)) { if (postNumber && (postNumber > 0)) {
url += "/" + postNumber; url += "/" + postNumber;
} }
@ -74,7 +77,7 @@ Discourse.Topic = Discourse.Model.extend({
}, },
totalUnread: function() { totalUnread: function() {
var count = (this.get('unread') || 0) + (this.get('new_posts') || 0); const count = (this.get('unread') || 0) + (this.get('new_posts') || 0);
return count > 0 ? count : null; return count > 0 ? count : null;
}.property('new_posts', 'unread'), }.property('new_posts', 'unread'),
@ -83,7 +86,7 @@ Discourse.Topic = Discourse.Model.extend({
}.property('url', 'last_read_post_number'), }.property('url', 'last_read_post_number'),
lastUnreadUrl: function() { lastUnreadUrl: function() {
var postNumber = Math.min(this.get('last_read_post_number') + 1, this.get('highest_post_number')); const postNumber = Math.min(this.get('last_read_post_number') + 1, this.get('highest_post_number'));
return this.urlForPostNumber(postNumber); return this.urlForPostNumber(postNumber);
}.property('url', 'last_read_post_number', 'highest_post_number'), }.property('url', 'last_read_post_number', 'highest_post_number'),
@ -107,12 +110,11 @@ Discourse.Topic = Discourse.Model.extend({
// tells us if we are still asynchronously flushing our "recently read" data. // tells us if we are still asynchronously flushing our "recently read" data.
// So take what the browser has seen into consideration. // So take what the browser has seen into consideration.
displayNewPosts: function() { displayNewPosts: function() {
var delta, result; const highestSeen = Discourse.Session.currentProp('highestSeenByTopic')[this.get('id')];
var highestSeen = Discourse.Session.currentProp('highestSeenByTopic')[this.get('id')];
if (highestSeen) { if (highestSeen) {
delta = highestSeen - this.get('last_read_post_number'); let delta = highestSeen - this.get('last_read_post_number');
if (delta > 0) { if (delta > 0) {
result = this.get('new_posts') - delta; let result = this.get('new_posts') - delta;
if (result < 0) { if (result < 0) {
result = 0; result = 0;
} }
@ -123,7 +125,7 @@ Discourse.Topic = Discourse.Model.extend({
}.property('new_posts', 'id'), }.property('new_posts', 'id'),
viewsHeat: function() { viewsHeat: function() {
var v = this.get('views'); const v = this.get('views');
if( v >= Discourse.SiteSettings.topic_views_heat_high ) return 'heatmap-high'; if( v >= Discourse.SiteSettings.topic_views_heat_high ) return 'heatmap-high';
if( v >= Discourse.SiteSettings.topic_views_heat_medium ) return 'heatmap-med'; if( v >= Discourse.SiteSettings.topic_views_heat_medium ) return 'heatmap-med';
if( v >= Discourse.SiteSettings.topic_views_heat_low ) return 'heatmap-low'; if( v >= Discourse.SiteSettings.topic_views_heat_low ) return 'heatmap-low';
@ -137,17 +139,17 @@ Discourse.Topic = Discourse.Model.extend({
isPrivateMessage: Em.computed.equal('archetype', 'private_message'), isPrivateMessage: Em.computed.equal('archetype', 'private_message'),
isBanner: Em.computed.equal('archetype', 'banner'), isBanner: Em.computed.equal('archetype', 'banner'),
toggleStatus: function(property) { toggleStatus(property) {
this.toggleProperty(property); this.toggleProperty(property);
this.saveStatus(property, this.get(property) ? true : false); this.saveStatus(property, this.get(property) ? true : false);
}, },
setStatus: function(property, value) { setStatus(property, value) {
this.set(property, value); this.set(property, value);
this.saveStatus(property, value); this.saveStatus(property, value);
}, },
saveStatus: function(property, value) { saveStatus(property, value) {
if (property === 'closed' && value === true) { if (property === 'closed' && value === true) {
this.set('details.auto_close_at', null); this.set('details.auto_close_at', null);
} }
@ -160,28 +162,28 @@ Discourse.Topic = Discourse.Model.extend({
}); });
}, },
makeBanner: function() { makeBanner() {
var self = this; const self = this;
return Discourse.ajax('/t/' + this.get('id') + '/make-banner', { type: 'PUT' }) return Discourse.ajax('/t/' + this.get('id') + '/make-banner', { type: 'PUT' })
.then(function () { self.set('archetype', 'banner'); }); .then(function () { self.set('archetype', 'banner'); });
}, },
removeBanner: function() { removeBanner() {
var self = this; const self = this;
return Discourse.ajax('/t/' + this.get('id') + '/remove-banner', { type: 'PUT' }) return Discourse.ajax('/t/' + this.get('id') + '/remove-banner', { type: 'PUT' })
.then(function () { self.set('archetype', 'regular'); }); .then(function () { self.set('archetype', 'regular'); });
}, },
estimatedReadingTime: function() { estimatedReadingTime: function() {
var wordCount = this.get('word_count'); const wordCount = this.get('word_count');
if (!wordCount) return; if (!wordCount) return;
// Avg for 500 words per minute when you account for skimming // Avg for 500 words per minute when you account for skimming
return Math.floor(wordCount / 500.0); return Math.floor(wordCount / 500.0);
}.property('word_count'), }.property('word_count'),
toggleBookmark: function() { toggleBookmark() {
var self = this, firstPost = this.get("postStream.posts")[0]; const self = this, firstPost = this.get("postStream.posts")[0];
this.toggleProperty('bookmarked'); this.toggleProperty('bookmarked');
if (this.get("postStream.firstPostPresent")) { firstPost.toggleProperty("bookmarked"); } if (this.get("postStream.firstPostPresent")) { firstPost.toggleProperty("bookmarked"); }
@ -194,8 +196,7 @@ Discourse.Topic = Discourse.Model.extend({
self.toggleProperty('bookmarked'); self.toggleProperty('bookmarked');
if (self.get("postStream.firstPostPresent")) { firstPost.toggleProperty('bookmarked'); } if (self.get("postStream.firstPostPresent")) { firstPost.toggleProperty('bookmarked'); }
var showGenericError = true; let showGenericError = true;
if (error && error.responseText) { if (error && error.responseText) {
try { try {
bootbox.alert($.parseJSON(error.responseText).errors); bootbox.alert($.parseJSON(error.responseText).errors);
@ -209,13 +210,7 @@ Discourse.Topic = Discourse.Model.extend({
}); });
}, },
/** createInvite(emailOrUsername, groupNames) {
Invite a user to this topic
@method createInvite
@param {String} emailOrUsername The email or username of the user to be invited
**/
createInvite: function(emailOrUsername, groupNames) {
return Discourse.ajax("/t/" + this.get('id') + "/invite", { return Discourse.ajax("/t/" + this.get('id') + "/invite", {
type: 'POST', type: 'POST',
data: { user: emailOrUsername, group_names: groupNames } data: { user: emailOrUsername, group_names: groupNames }
@ -223,7 +218,7 @@ Discourse.Topic = Discourse.Model.extend({
}, },
// Delete this topic // Delete this topic
destroy: function(deleted_by) { destroy(deleted_by) {
this.setProperties({ this.setProperties({
deleted_at: new Date(), deleted_at: new Date(),
deleted_by: deleted_by, deleted_by: deleted_by,
@ -237,7 +232,7 @@ Discourse.Topic = Discourse.Model.extend({
}, },
// Recover this topic if deleted // Recover this topic if deleted
recover: function() { recover() {
this.setProperties({ this.setProperties({
deleted_at: null, deleted_at: null,
deleted_by: null, deleted_by: null,
@ -248,14 +243,14 @@ Discourse.Topic = Discourse.Model.extend({
}, },
// Update our attributes from a JSON result // Update our attributes from a JSON result
updateFromJson: function(json) { updateFromJson(json) {
this.get('details').updateFromJson(json.details); this.get('details').updateFromJson(json.details);
var keys = Object.keys(json); const keys = Object.keys(json);
keys.removeObject('details'); keys.removeObject('details');
keys.removeObject('post_stream'); keys.removeObject('post_stream');
var topic = this; const topic = this;
keys.forEach(function (key) { keys.forEach(function (key) {
topic.set(key, json[key]); topic.set(key, json[key]);
}); });
@ -266,13 +261,8 @@ Discourse.Topic = Discourse.Model.extend({
return this.get('pinned') && this.get('category.isUncategorizedCategory'); return this.get('pinned') && this.get('category.isUncategorizedCategory');
}.property('pinned', 'category.isUncategorizedCategory'), }.property('pinned', 'category.isUncategorizedCategory'),
/** clearPin() {
Clears the pin from a topic for the currently logged in user const topic = this;
@method clearPin
**/
clearPin: function() {
var topic = this;
// Clear the pin optimistically from the object // Clear the pin optimistically from the object
topic.set('pinned', false); topic.set('pinned', false);
@ -287,7 +277,7 @@ Discourse.Topic = Discourse.Model.extend({
}); });
}, },
togglePinnedForUser: function() { togglePinnedForUser() {
if (this.get('pinned')) { if (this.get('pinned')) {
this.clearPin(); this.clearPin();
} else { } else {
@ -295,13 +285,8 @@ Discourse.Topic = Discourse.Model.extend({
} }
}, },
/** rePin() {
Re-pins a topic with a cleared pin const topic = this;
@method rePin
**/
rePin: function() {
var topic = this;
// Clear the pin optimistically from the object // Clear the pin optimistically from the object
topic.set('pinned', true); topic.set('pinned', true);
@ -317,12 +302,12 @@ Discourse.Topic = Discourse.Model.extend({
}, },
// Is the reply to a post directly below it? // Is the reply to a post directly below it?
isReplyDirectlyBelow: function(post) { isReplyDirectlyBelow(post) {
var posts = this.get('postStream.posts'); const posts = this.get('postStream.posts');
var postNumber = post.get('post_number'); const postNumber = post.get('post_number');
if (!posts) return; if (!posts) return;
var postBelow = posts[posts.indexOf(post) + 1]; const postBelow = posts[posts.indexOf(post) + 1];
// If the post directly below's reply_to_post_number is our post number or we are quoted, // If the post directly below's reply_to_post_number is our post number or we are quoted,
// it's considered directly below. // it's considered directly below.
@ -340,7 +325,7 @@ Discourse.Topic = Discourse.Model.extend({
hasExcerpt: Em.computed.and('pinned', 'excerptNotEmpty'), hasExcerpt: Em.computed.and('pinned', 'excerptNotEmpty'),
excerptTruncated: function() { excerptTruncated: function() {
var e = this.get('excerpt'); const e = this.get('excerpt');
return( e && e.substr(e.length - 8,8) === '&hellip;' ); return( e && e.substr(e.length - 8,8) === '&hellip;' );
}.property('excerpt'), }.property('excerpt'),
@ -349,7 +334,7 @@ Discourse.Topic = Discourse.Model.extend({
}); });
Discourse.Topic.reopenClass({ Topic.reopenClass({
NotificationLevel: { NotificationLevel: {
WATCHING: 3, WATCHING: 3,
TRACKING: 2, TRACKING: 2,
@ -357,13 +342,13 @@ Discourse.Topic.reopenClass({
MUTED: 0 MUTED: 0
}, },
createActionSummary: function(result) { createActionSummary(result) {
if (result.actions_summary) { if (result.actions_summary) {
var lookup = Em.Object.create(); const lookup = Em.Object.create();
result.actions_summary = result.actions_summary.map(function(a) { result.actions_summary = result.actions_summary.map(function(a) {
a.post = result; a.post = result;
a.actionType = Discourse.Site.current().postActionTypeById(a.id); a.actionType = Discourse.Site.current().postActionTypeById(a.id);
var actionSummary = Discourse.ActionSummary.create(a); const actionSummary = Discourse.ActionSummary.create(a);
lookup.set(a.actionType.get('name_key'), actionSummary); lookup.set(a.actionType.get('name_key'), actionSummary);
return actionSummary; return actionSummary;
}); });
@ -371,7 +356,7 @@ Discourse.Topic.reopenClass({
} }
}, },
update: function(topic, props) { update(topic, props) {
props = JSON.parse(JSON.stringify(props)) || {}; props = JSON.parse(JSON.stringify(props)) || {};
// We support `category_id` and `categoryId` for compatibility // We support `category_id` and `categoryId` for compatibility
@ -384,7 +369,7 @@ Discourse.Topic.reopenClass({
// allows us to make a distinction between arrays that were not // allows us to make a distinction between arrays that were not
// sent and arrays that we specifically want to be empty. // sent and arrays that we specifically want to be empty.
Object.keys(props).forEach(function(k) { Object.keys(props).forEach(function(k) {
var v = props[k]; const v = props[k];
if (v instanceof Array && v.length === 0) { if (v instanceof Array && v.length === 0) {
props[k + '_empty_array'] = true; props[k + '_empty_array'] = true;
} }
@ -400,24 +385,16 @@ Discourse.Topic.reopenClass({
}); });
}, },
create: function() { create() {
var result = this._super.apply(this, arguments); const result = this._super.apply(this, arguments);
this.createActionSummary(result); this.createActionSummary(result);
return result; return result;
}, },
/** findSimilarTo(title, body) {
Find similar topics to a given title and body
@method findSimilar
@param {String} title The current title
@param {String} body The current body
@returns A promise that will resolve to the topics
**/
findSimilarTo: function(title, body) {
return Discourse.ajax("/topics/similar_to", { data: {title: title, raw: body} }).then(function (results) { return Discourse.ajax("/topics/similar_to", { data: {title: title, raw: body} }).then(function (results) {
if (Array.isArray(results)) { if (Array.isArray(results)) {
return results.map(function(topic) { return Discourse.Topic.create(topic); }); return results.map(function(topic) { return Topic.create(topic); });
} else { } else {
return Ember.A(); return Ember.A();
} }
@ -425,14 +402,13 @@ Discourse.Topic.reopenClass({
}, },
// Load a topic, but accepts a set of filters // Load a topic, but accepts a set of filters
find: function(topicId, opts) { find(topicId, opts) {
var url = Discourse.getURL("/t/") + topicId; let url = Discourse.getURL("/t/") + topicId;
if (opts.nearPost) { if (opts.nearPost) {
url += "/" + opts.nearPost; url += "/" + opts.nearPost;
} }
var data = {}; const data = {};
if (opts.postsAfter) { if (opts.postsAfter) {
data.posts_after = opts.postsAfter; data.posts_after = opts.postsAfter;
} }
@ -461,8 +437,8 @@ Discourse.Topic.reopenClass({
return Discourse.ajax(url + ".json", {data: data}); return Discourse.ajax(url + ".json", {data: data});
}, },
mergeTopic: function(topicId, destinationTopicId) { mergeTopic(topicId, destinationTopicId) {
var promise = Discourse.ajax("/t/" + topicId + "/merge-topic", { const promise = Discourse.ajax("/t/" + topicId + "/merge-topic", {
type: 'POST', type: 'POST',
data: {destination_topic_id: destinationTopicId} data: {destination_topic_id: destinationTopicId}
}).then(function (result) { }).then(function (result) {
@ -472,8 +448,8 @@ Discourse.Topic.reopenClass({
return promise; return promise;
}, },
movePosts: function(topicId, opts) { movePosts(topicId, opts) {
var promise = Discourse.ajax("/t/" + topicId + "/move-posts", { const promise = Discourse.ajax("/t/" + topicId + "/move-posts", {
type: 'POST', type: 'POST',
data: opts data: opts
}).then(function (result) { }).then(function (result) {
@ -483,8 +459,8 @@ Discourse.Topic.reopenClass({
return promise; return promise;
}, },
changeOwners: function(topicId, opts) { changeOwners(topicId, opts) {
var promise = Discourse.ajax("/t/" + topicId + "/change-owner", { const promise = Discourse.ajax("/t/" + topicId + "/change-owner", {
type: 'POST', type: 'POST',
data: opts data: opts
}).then(function (result) { }).then(function (result) {
@ -494,7 +470,7 @@ Discourse.Topic.reopenClass({
return promise; return promise;
}, },
bulkOperation: function(topics, operation) { bulkOperation(topics, operation) {
return Discourse.ajax("/topics/bulk", { return Discourse.ajax("/topics/bulk", {
type: 'PUT', type: 'PUT',
data: { data: {
@ -504,8 +480,8 @@ Discourse.Topic.reopenClass({
}); });
}, },
bulkOperationByFilter: function(filter, operation, categoryId) { bulkOperationByFilter(filter, operation, categoryId) {
var data = { filter: filter, operation: operation }; const data = { filter: filter, operation: operation };
if (categoryId) data['category_id'] = categoryId; if (categoryId) data['category_id'] = categoryId;
return Discourse.ajax("/topics/bulk", { return Discourse.ajax("/topics/bulk", {
type: 'PUT', type: 'PUT',
@ -513,12 +489,13 @@ Discourse.Topic.reopenClass({
}); });
}, },
resetNew: function() { resetNew() {
return Discourse.ajax("/topics/reset-new", {type: 'PUT'}); return Discourse.ajax("/topics/reset-new", {type: 'PUT'});
}, },
idForSlug: function(slug) { idForSlug(slug) {
return Discourse.ajax("/t/id_for/" + slug); return Discourse.ajax("/t/id_for/" + slug);
} }
}); });
export default Topic;

View File

@ -1,6 +1,8 @@
import Topic from 'discourse/models/topic';
export default Discourse.Route.extend({ export default Discourse.Route.extend({
model: function(params) { model: function(params) {
return Discourse.Topic.idForSlug(params.slug); return Topic.idForSlug(params.slug);
}, },
afterModel: function(result) { afterModel: function(result) {

View File

@ -4,6 +4,7 @@ var isTransitioning = false,
SCROLL_DELAY = 500; SCROLL_DELAY = 500;
import ShowFooter from "discourse/mixins/show-footer"; import ShowFooter from "discourse/mixins/show-footer";
import Topic from 'discourse/models/topic';
var TopicRoute = Discourse.Route.extend(ShowFooter, { var TopicRoute = Discourse.Route.extend(ShowFooter, {
redirect: function() { return this.redirectIfLoginRequired(); }, redirect: function() { return this.redirectIfLoginRequired(); },
@ -163,7 +164,7 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, {
return topic; return topic;
}); });
} else { } else {
return this.setupParams(Discourse.Topic.create(_.omit(params, 'username_filters', 'filter')), queryParams); return this.setupParams(Topic.create(_.omit(params, 'username_filters', 'filter')), queryParams);
} }
}, },

View File

@ -154,8 +154,6 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
var $wmdPreview = $('#wmd-preview'); var $wmdPreview = $('#wmd-preview');
if ($wmdPreview.length === 0) return; if ($wmdPreview.length === 0) return;
Discourse.SyntaxHighlighting.apply($wmdPreview);
var post = this.get('model.post'), var post = this.get('model.post'),
refresh = false; refresh = false;

View File

@ -264,8 +264,6 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
this._showLinkCounts(); this._showLinkCounts();
Discourse.ScreenTrack.current().track(this.$().prop('id'), postNumber); Discourse.ScreenTrack.current().track(this.$().prop('id'), postNumber);
Discourse.SyntaxHighlighting.apply($post);
Discourse.Lightbox.apply($post);
this.trigger('postViewInserted', $post); this.trigger('postViewInserted', $post);

View File

@ -28,6 +28,8 @@
//= require ./discourse/models/model //= require ./discourse/models/model
//= require ./discourse/models/user_action //= require ./discourse/models/user_action
//= require ./discourse/models/composer //= require ./discourse/models/composer
//= require ./discourse/models/post-stream
//= require ./discourse/models/topic-details
//= require ./discourse/models/topic //= require ./discourse/models/topic
//= require ./discourse/models/top-period //= require ./discourse/models/top-period
//= require ./discourse/controllers/controller //= require ./discourse/controllers/controller

View File

@ -3,8 +3,10 @@ moduleFor('controller:topic', 'controller:topic', {
'controller:search', 'controller:topic-progress', 'controller:application'] 'controller:search', 'controller:topic-progress', 'controller:application']
}); });
import Topic from 'discourse/models/topic';
var buildTopic = function() { var buildTopic = function() {
return Discourse.Topic.create({ return Topic.create({
title: "Qunit Test Topic", title: "Qunit Test Topic",
participants: [ participants: [
{id: 1234, {id: 1234,

View File

@ -1,5 +1,7 @@
module("Discourse.SelectedPostsCount"); module("Discourse.SelectedPostsCount");
import Topic from 'discourse/models/topic';
var buildTestObj = function(params) { var buildTestObj = function(params) {
return Ember.Object.createWithMixins(Discourse.SelectedPostsCount, params || {}); return Ember.Object.createWithMixins(Discourse.SelectedPostsCount, params || {});
}; };
@ -26,7 +28,7 @@ test("when all posts are selected and there is a posts_count", function() {
test("when all posts are selected and there is topic with a posts_count", function() { test("when all posts are selected and there is topic with a posts_count", function() {
var testObj = buildTestObj({ var testObj = buildTestObj({
allPostsSelected: true, allPostsSelected: true,
topic: Discourse.Topic.create({ posts_count: 3456 }) topic: Topic.create({ posts_count: 3456 })
}); });
equal(testObj.get('selectedPostsCount'), 3456, "It returns the topic's posts_count"); equal(testObj.get('selectedPostsCount'), 3456, "It returns the topic's posts_count");

View File

@ -1,7 +1,10 @@
module("Discourse.PostStream"); module("model:post-stream");
import PostStream from 'discourse/models/post-stream';
import Topic from 'discourse/models/topic';
var buildStream = function(id, stream) { var buildStream = function(id, stream) {
var topic = Discourse.Topic.create({id: id, chunk_size: 5}); var topic = Topic.create({id: id, chunk_size: 5});
var ps = topic.get('postStream'); var ps = topic.get('postStream');
if (stream) { if (stream) {
ps.set('stream', stream); ps.set('stream', stream);
@ -12,7 +15,7 @@ var buildStream = function(id, stream) {
var participant = {username: 'eviltrout'}; var participant = {username: 'eviltrout'};
test('create', function() { test('create', function() {
ok(Discourse.PostStream.create(), 'it can be created with no parameters'); ok(PostStream.create(), 'it can be created with no parameters');
}); });
test('defaults', function() { test('defaults', function() {

View File

@ -1,7 +1,9 @@
module("Discourse.TopicDetails"); module("model:topic-details");
import Topic from 'discourse/models/topic';
var buildDetails = function(id) { var buildDetails = function(id) {
var topic = Discourse.Topic.create({id: id}); var topic = Topic.create({id: id});
return topic.get('details'); return topic.get('details');
}; };
@ -20,13 +22,9 @@ test('updateFromJson', function() {
}); });
equal(details.get('suggested_topics.length'), 2, 'it loaded the suggested_topics'); equal(details.get('suggested_topics.length'), 2, 'it loaded the suggested_topics');
containsInstance(details.get('suggested_topics'), Discourse.Topic); containsInstance(details.get('suggested_topics'), Topic);
equal(details.get('allowed_users.length'), 1, 'it loaded the allowed users'); equal(details.get('allowed_users.length'), 1, 'it loaded the allowed users');
containsInstance(details.get('allowed_users'), Discourse.User); containsInstance(details.get('allowed_users'), Discourse.User);
}); });

View File

@ -1,20 +1,22 @@
module("Discourse.Topic"); module("model:topic");
import Topic from 'discourse/models/topic';
test("defaults", function() { test("defaults", function() {
var topic = Discourse.Topic.create({id: 1234}); var topic = Topic.create({id: 1234});
blank(topic.get('deleted_at'), 'deleted_at defaults to blank'); blank(topic.get('deleted_at'), 'deleted_at defaults to blank');
blank(topic.get('deleted_by'), 'deleted_by defaults to blank'); blank(topic.get('deleted_by'), 'deleted_by defaults to blank');
}); });
test('has details', function() { test('has details', function() {
var topic = Discourse.Topic.create({id: 1234}); var topic = Topic.create({id: 1234});
var topicDetails = topic.get('details'); var topicDetails = topic.get('details');
present(topicDetails, "a topic has topicDetails after we create it"); present(topicDetails, "a topic has topicDetails after we create it");
equal(topicDetails.get('topic'), topic, "the topicDetails has a reference back to the topic"); equal(topicDetails.get('topic'), topic, "the topicDetails has a reference back to the topic");
}); });
test('has a postStream', function() { test('has a postStream', function() {
var topic = Discourse.Topic.create({id: 1234}); var topic = Topic.create({id: 1234});
var postStream = topic.get('postStream'); var postStream = topic.get('postStream');
present(postStream, "a topic has a postStream after we create it"); present(postStream, "a topic has a postStream after we create it");
equal(postStream.get('topic'), topic, "the postStream has a reference back to the topic"); equal(postStream.get('topic'), topic, "the postStream has a reference back to the topic");
@ -24,13 +26,13 @@ test('has a postStream', function() {
test('category relationship', function() { test('category relationship', function() {
// It finds the category by id // It finds the category by id
var category = Discourse.Category.list()[0], var category = Discourse.Category.list()[0],
topic = Discourse.Topic.create({id: 1111, category_id: category.get('id') }); topic = Topic.create({id: 1111, category_id: category.get('id') });
equal(topic.get('category'), category); equal(topic.get('category'), category);
}); });
test("updateFromJson", function() { test("updateFromJson", function() {
var topic = Discourse.Topic.create({id: 1234}), var topic = Topic.create({id: 1234}),
category = Discourse.Category.list()[0]; category = Discourse.Category.list()[0];
topic.updateFromJson({ topic.updateFromJson({
@ -48,7 +50,7 @@ test("updateFromJson", function() {
test("destroy", function() { test("destroy", function() {
var user = Discourse.User.create({username: 'eviltrout'}); var user = Discourse.User.create({username: 'eviltrout'});
var topic = Discourse.Topic.create({id: 1234}); var topic = Topic.create({id: 1234});
sandbox.stub(Discourse, 'ajax'); sandbox.stub(Discourse, 'ajax');
@ -60,7 +62,7 @@ test("destroy", function() {
test("recover", function() { test("recover", function() {
var user = Discourse.User.create({username: 'eviltrout'}); var user = Discourse.User.create({username: 'eviltrout'});
var topic = Discourse.Topic.create({id: 1234, deleted_at: new Date(), deleted_by: user}); var topic = Topic.create({id: 1234, deleted_at: new Date(), deleted_by: user});
sandbox.stub(Discourse, 'ajax'); sandbox.stub(Discourse, 'ajax');