This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/models/topic.js
Robin Ward 6048c9e100 Let's switch back to using post numbers rather than last for links to
the last post. We'll keep support for the old format, but start linking
directly to the last post.
2014-06-06 10:14:43 -04:00

471 lines
13 KiB
JavaScript

/**
A data model representing a Topic
@class Topic
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.Topic = Discourse.Model.extend({
postStream: function() {
return Discourse.PostStream.create({topic: this});
}.property(),
details: function() {
return Discourse.TopicDetails.create({topic: this});
}.property(),
invisible: Em.computed.not('visible'),
deleted: Em.computed.notEmpty('deleted_at'),
canConvertToRegular: function() {
var a = this.get('archetype');
return a !== 'regular' && a !== 'private_message';
}.property('archetype'),
convertArchetype: function() {
var a = this.get('archetype');
if (a !== 'regular' && a !== 'private_message') {
this.set('archetype', 'regular');
return Discourse.ajax(this.get('url'), {
type: 'PUT',
data: {archetype: 'regular'}
});
}
},
searchContext: function() {
return ({ type: 'topic', id: this.get('id') });
}.property('id'),
category: function() {
var categoryId = this.get('category_id');
if (categoryId) {
return Discourse.Category.list().findProperty('id', categoryId);
}
var categoryName = this.get('categoryName');
if (categoryName) {
return Discourse.Category.list().findProperty('name', categoryName);
}
return null;
}.property('category_id', 'categoryName'),
categoryClass: function() {
return 'category-' + Discourse.Category.slugFor(this.get('category'));
}.property('category'),
shareUrl: function(){
var user = Discourse.User.current();
return this.get('url') + (user ? '?u=' + user.get('username_lower') : '');
}.property('url'),
url: function() {
var slug = this.get('slug');
if (slug.trim().length === 0) {
slug = "topic";
}
return Discourse.getURL("/t/") + slug + "/" + (this.get('id'));
}.property('id', 'slug'),
// Helper to build a Url with a post number
urlForPostNumber: function(postNumber) {
var url = this.get('url');
if (postNumber && (postNumber > 0)) {
url += "/" + postNumber;
}
return url;
},
lastReadUrl: function() {
return this.urlForPostNumber(this.get('last_read_post_number'));
}.property('url', 'last_read_post_number'),
lastUnreadUrl: function() {
var postNumber = Math.min(this.get('last_read_post_number') + 1, this.get('highest_post_number'));
return this.urlForPostNumber(postNumber);
}.property('url', 'last_read_post_number', 'highest_post_number'),
lastPostUrl: function() {
return this.urlForPostNumber(this.get('highest_post_number'));
}.property('url', 'highest_post_number'),
firstPostUrl: function () {
return this.urlForPostNumber(1);
}.property('url'),
lastPosterUrl: function() {
return Discourse.getURL("/users/") + this.get("last_poster.username");
}.property('last_poster'),
// The amount of new posts to display. It might be different than what the server
// tells us if we are still asynchronously flushing our "recently read" data.
// So take what the browser has seen into consideration.
displayNewPosts: function() {
var delta, result;
var highestSeen = Discourse.Session.currentProp('highestSeenByTopic')[this.get('id')];
if (highestSeen) {
delta = highestSeen - this.get('last_read_post_number');
if (delta > 0) {
result = this.get('new_posts') - delta;
if (result < 0) {
result = 0;
}
return result;
}
}
return this.get('new_posts');
}.property('new_posts', 'id'),
viewsHeat: function() {
var v = this.get('views');
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_low ) return 'heatmap-low';
return null;
}.property('views'),
archetypeObject: function() {
return Discourse.Site.currentProp('archetypes').findProperty('id', this.get('archetype'));
}.property('archetype'),
isPrivateMessage: Em.computed.equal('archetype', 'private_message'),
toggleStatus: function(property) {
this.toggleProperty(property);
this.saveStatus(property, this.get(property) ? true : false);
},
setStatus: function(property, value) {
this.set(property, value);
this.saveStatus(property, value);
},
saveStatus: function(property, value) {
if (property === 'closed' && value === true) {
this.set('details.auto_close_at', null);
}
if (property === 'pinned') {
this.set('pinned_at', value ? moment() : null);
}
return Discourse.ajax(this.get('url') + "/status", {
type: 'PUT',
data: {status: property, enabled: value ? 'true' : 'false' }
});
},
starTooltipKey: function() {
return this.get('starred') ? 'starred.help.unstar' : 'starred.help.star';
}.property('starred'),
starTooltip: function() {
return I18n.t(this.get('starTooltipKey'));
}.property('starTooltipKey'),
estimatedReadingTime: function() {
var wordCount = this.get('word_count');
if (!wordCount) return;
// Avg for 500 words per minute when you account for skimming
return Math.floor(wordCount / 500.0);
}.property('word_count'),
toggleStar: function() {
var topic = this;
topic.toggleProperty('starred');
return Discourse.ajax({
url: "" + (this.get('url')) + "/star",
type: 'PUT',
data: { starred: topic.get('starred') ? true : false }
}).then(null, function (error) {
topic.toggleProperty('starred');
if (error && error.responseText) {
bootbox.alert($.parseJSON(error.responseText).errors);
} else {
bootbox.alert(I18n.t('generic_error'));
}
});
},
// Save any changes we've made to the model
save: function() {
// Don't save unless we can
if (!this.get('details.can_edit')) return;
return Discourse.ajax(this.get('url'), {
type: 'PUT',
data: { title: this.get('title'), category: this.get('category.name') }
});
},
// Reset our read data for this topic
resetRead: function() {
return Discourse.ajax("/t/" + this.get('id') + "/timings", {
type: 'DELETE'
});
},
/**
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", {
type: 'POST',
data: { user: emailOrUsername, group_names: groupNames }
});
},
// Delete this topic
destroy: function(deleted_by) {
this.setProperties({
deleted_at: new Date(),
deleted_by: deleted_by,
'details.can_delete': false,
'details.can_recover': true
});
return Discourse.ajax("/t/" + this.get('id'), { type: 'DELETE' });
},
// Recover this topic if deleted
recover: function() {
this.setProperties({
deleted_at: null,
deleted_by: null,
'details.can_delete': true,
'details.can_recover': false
});
return Discourse.ajax("/t/" + this.get('id') + "/recover", { type: 'PUT' });
},
// Update our attributes from a JSON result
updateFromJson: function(json) {
this.get('details').updateFromJson(json.details);
var keys = Object.keys(json);
keys.removeObject('details');
keys.removeObject('post_stream');
var topic = this;
keys.forEach(function (key) {
topic.set(key, json[key]);
});
},
isPinnedUncategorized: function() {
return this.get('pinned') && this.get('category.isUncategorizedCategory');
}.property('pinned', 'category.isUncategorizedCategory'),
/**
Clears the pin from a topic for the currently logged in user
@method clearPin
**/
clearPin: function() {
var topic = this;
// Clear the pin optimistically from the object
topic.set('pinned', false);
topic.set('unpinned', true);
Discourse.ajax("/t/" + this.get('id') + "/clear-pin", {
type: 'PUT'
}).then(null, function() {
// On error, put the pin back
topic.set('pinned', true);
topic.set('unpinned', false);
});
},
/**
Re-pins a topic with a cleared pin
@method rePin
**/
rePin: function() {
var topic = this;
// Clear the pin optimistically from the object
topic.set('pinned', true);
topic.set('unpinned', false);
Discourse.ajax("/t/" + this.get('id') + "/re-pin", {
type: 'PUT'
}).then(null, function() {
// On error, put the pin back
topic.set('pinned', true);
topic.set('unpinned', false);
});
},
// Is the reply to a post directly below it?
isReplyDirectlyBelow: function(post) {
var posts = this.get('postStream.posts');
if (!posts) return;
var postBelow = posts[posts.indexOf(post) + 1];
// If the post directly below's reply_to_post_number is our post number, it's
// considered directly below.
return postBelow && postBelow.get('reply_to_post_number') === post.get('post_number');
},
excerptNotEmpty: Em.computed.notEmpty('excerpt'),
hasExcerpt: Em.computed.and('pinned', 'excerptNotEmpty'),
excerptTruncated: function() {
var e = this.get('excerpt');
return( e && e.substr(e.length - 8,8) === '&hellip;' );
}.property('excerpt'),
readLastPost: Discourse.computed.propertyEqual('last_read_post_number', 'highest_post_number'),
canCleanPin: Em.computed.and('pinned', 'readLastPost')
});
Discourse.Topic.reopenClass({
NotificationLevel: {
WATCHING: 3,
TRACKING: 2,
REGULAR: 1,
MUTED: 0
},
createActionSummary: function(result) {
if (result.actions_summary) {
var lookup = Em.Object.create();
result.actions_summary = result.actions_summary.map(function(a) {
a.post = result;
a.actionType = Discourse.Site.current().postActionTypeById(a.id);
var actionSummary = Discourse.ActionSummary.create(a);
lookup.set(a.actionType.get('name_key'), actionSummary);
return actionSummary;
});
result.set('actionByName', lookup);
}
},
create: function() {
var result = this._super.apply(this, arguments);
this.createActionSummary(result);
return result;
},
/**
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) {
if (Array.isArray(results)) {
return results.map(function(topic) { return Discourse.Topic.create(topic); });
} else {
return Ember.A();
}
});
},
// Load a topic, but accepts a set of filters
find: function(topicId, opts) {
var url = Discourse.getURL("/t/") + topicId;
if (opts.nearPost) {
url += "/" + opts.nearPost;
}
var data = {};
if (opts.postsAfter) {
data.posts_after = opts.postsAfter;
}
if (opts.postsBefore) {
data.posts_before = opts.postsBefore;
}
if (opts.trackVisit) {
data.track_visit = true;
}
// Add username filters if we have them
if (opts.userFilters && opts.userFilters.length > 0) {
data.username_filters = [];
opts.userFilters.forEach(function(username) {
data.username_filters.push(username);
});
}
// Add the summary of filter if we have it
if (opts.summary === true) {
data.summary = true;
}
// Check the preload store. If not, load it via JSON
return Discourse.ajax(url + ".json", {data: data});
},
mergeTopic: function(topicId, destinationTopicId) {
var promise = Discourse.ajax("/t/" + topicId + "/merge-topic", {
type: 'POST',
data: {destination_topic_id: destinationTopicId}
}).then(function (result) {
if (result.success) return result;
promise.reject(new Error("error merging topic"));
});
return promise;
},
movePosts: function(topicId, opts) {
var promise = Discourse.ajax("/t/" + topicId + "/move-posts", {
type: 'POST',
data: opts
}).then(function (result) {
if (result.success) return result;
promise.reject(new Error("error moving posts topic"));
});
return promise;
},
changeOwners: function(topicId, opts) {
var promise = Discourse.ajax("/t/" + topicId + "/change-owner", {
type: 'POST',
data: opts
}).then(function (result) {
if (result.success) return result;
promise.reject(new Error("error changing ownership of posts"));
});
return promise;
},
bulkOperation: function(topics, operation) {
return Discourse.ajax("/topics/bulk", {
type: 'PUT',
data: {
topic_ids: topics.map(function(t) { return t.get('id'); }),
operation: operation
}
});
},
bulkOperationByFilter: function(filter, operation) {
return Discourse.ajax("/topics/bulk", {
type: 'PUT',
data: { filter: filter, operation: operation }
});
},
resetNew: function() {
return Discourse.ajax("/topics/reset-new", {type: 'PUT'});
}
});