2013-02-22 15:41:12 -05:00
/ * *
A data model representing a Topic
@ class Topic
@ extends Discourse . Model
@ namespace Discourse
@ module Discourse
* * /
Discourse . Topic = Discourse . Model . extend ( {
categoriesBinding : 'Discourse.site.categories' ,
fewParticipants : ( function ( ) {
if ( ! this . present ( 'participants' ) ) return null ;
return this . get ( 'participants' ) . slice ( 0 , 3 ) ;
} ) . property ( 'participants' ) ,
canConvertToRegular : ( function ( ) {
var a = this . get ( 'archetype' ) ;
return a !== 'regular' && a !== 'private_message' ;
} ) . property ( 'archetype' ) ,
convertArchetype : function ( archetype ) {
var a ;
a = this . get ( 'archetype' ) ;
if ( a !== 'regular' && a !== 'private_message' ) {
this . set ( 'archetype' , 'regular' ) ;
2013-03-05 15:39:21 -05:00
return $ . post ( this . get ( 'url' ) , {
2013-02-22 15:41:12 -05:00
_method : 'put' ,
archetype : 'regular'
2013-02-20 13:15:50 -05:00
} ) ;
2013-02-22 15:41:12 -05:00
}
} ,
category : ( function ( ) {
if ( this . get ( 'categories' ) ) {
return this . get ( 'categories' ) . findProperty ( 'name' , this . get ( 'categoryName' ) ) ;
}
} ) . property ( 'categoryName' , 'categories' ) ,
url : ( function ( ) {
var slug = this . get ( 'slug' ) ;
if ( slug . isBlank ( ) ) {
slug = "topic" ;
}
return "/t/" + slug + "/" + ( this . get ( 'id' ) ) ;
} ) . property ( 'id' , 'slug' ) ,
// Helper to build a Url with a post number
urlForPostNumber : function ( postNumber ) {
var url ;
url = this . get ( 'url' ) ;
if ( postNumber && ( postNumber > 1 ) ) {
url += "/" + postNumber ;
}
return url ;
} ,
lastReadUrl : ( function ( ) {
return this . urlForPostNumber ( this . get ( 'last_read_post_number' ) ) ;
} ) . property ( 'url' , 'last_read_post_number' ) ,
lastPostUrl : ( function ( ) {
return this . urlForPostNumber ( this . get ( 'highest_post_number' ) ) ;
} ) . property ( 'url' , 'highest_post_number' ) ,
// The last post in the topic
lastPost : function ( ) {
return this . get ( 'posts' ) . last ( ) ;
} ,
postsChanged : ( function ( ) {
var last , posts ;
posts = this . get ( 'posts' ) ;
last = posts . last ( ) ;
if ( ! ( last && last . set && ! last . lastPost ) ) return ;
posts . each ( function ( p ) {
if ( p . lastPost ) return p . set ( 'lastPost' , false ) ;
} ) ;
last . set ( 'lastPost' , true ) ;
return true ;
} ) . observes ( 'posts.@each' , 'posts' ) ,
// 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 , highestSeen , result ;
if ( highestSeen = Discourse . get ( 'highestSeenByTopic' ) [ this . get ( 'id' ) ] ) {
delta = highestSeen - this . get ( 'last_read_post_number' ) ;
if ( delta > 0 ) {
result = this . get ( 'new_posts' ) - delta ;
if ( result < 0 ) {
result = 0 ;
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
return result ;
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
}
return this . get ( 'new_posts' ) ;
} ) . property ( 'new_posts' , 'id' ) ,
// The coldmap class for the age of the topic
ageCold : ( function ( ) {
var createdAt , createdAtDays , daysSinceEpoch , lastPost , nowDays ;
if ( ! ( lastPost = this . get ( 'last_posted_at' ) ) ) return ;
if ( ! ( createdAt = this . get ( 'created_at' ) ) ) return ;
daysSinceEpoch = function ( dt ) {
// 1000 * 60 * 60 * 24 = days since epoch
return dt . getTime ( ) / 86400000 ;
} ;
// Show heat on age
nowDays = daysSinceEpoch ( new Date ( ) ) ;
createdAtDays = daysSinceEpoch ( new Date ( createdAt ) ) ;
if ( daysSinceEpoch ( new Date ( lastPost ) ) > nowDays - 90 ) {
if ( createdAtDays < nowDays - 60 ) return 'coldmap-high' ;
if ( createdAtDays < nowDays - 30 ) return 'coldmap-med' ;
if ( createdAtDays < nowDays - 14 ) return 'coldmap-low' ;
}
return null ;
} ) . property ( 'age' , 'created_at' ) ,
archetypeObject : ( function ( ) {
return Discourse . get ( 'site.archetypes' ) . findProperty ( 'id' , this . get ( 'archetype' ) ) ;
} ) . property ( 'archetype' ) ,
isPrivateMessage : ( function ( ) {
return this . get ( 'archetype' ) === 'private_message' ;
} ) . property ( 'archetype' ) ,
// Does this topic only have a single post?
singlePost : ( function ( ) {
return this . get ( 'posts_count' ) === 1 ;
} ) . property ( 'posts_count' ) ,
toggleStatus : function ( property ) {
this . toggleProperty ( property ) ;
2013-03-05 15:39:21 -05:00
return $ . post ( "" + ( this . get ( 'url' ) ) + "/status" , {
2013-02-22 15:41:12 -05:00
_method : 'put' ,
status : property ,
enabled : this . get ( property ) ? 'true' : 'false'
} ) ;
} ,
toggleStar : function ( ) {
2013-02-25 18:38:11 -05:00
var topic = this ;
topic . toggleProperty ( 'starred' ) ;
2013-03-05 15:39:21 -05:00
return $ . ajax ( {
2013-02-22 15:41:12 -05:00
url : "" + ( this . get ( 'url' ) ) + "/star" ,
type : 'PUT' ,
2013-02-25 18:38:11 -05:00
data : { starred : topic . get ( 'starred' ) ? true : false } ,
2013-02-22 15:41:12 -05:00
error : function ( error ) {
2013-02-25 18:38:11 -05:00
topic . toggleProperty ( 'starred' ) ;
2013-03-05 15:39:21 -05:00
var errors = $ . parseJSON ( error . responseText ) . errors ;
2013-02-22 15:41:12 -05:00
return bootbox . alert ( errors [ 0 ] ) ;
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
} ) ;
} ,
// Save any changes we've made to the model
save : function ( ) {
// Don't save unless we can
if ( ! this . get ( 'can_edit' ) ) return ;
2013-03-05 15:39:21 -05:00
return $ . post ( this . get ( 'url' ) , {
2013-02-22 15:41:12 -05:00
_method : 'put' ,
title : this . get ( 'title' ) ,
category : this . get ( 'category.name' )
} ) ;
} ,
// Reset our read data for this topic
resetRead : function ( callback ) {
2013-03-05 15:39:21 -05:00
return $ . ajax ( "/t/" + ( this . get ( 'id' ) ) + "/timings" , {
2013-02-22 15:41:12 -05:00
type : 'DELETE' ,
success : function ( ) {
return typeof callback === "function" ? callback ( ) : void 0 ;
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
} ) ;
} ,
// Invite a user to this topic
inviteUser : function ( user ) {
2013-03-05 15:39:21 -05:00
return $ . ajax ( {
2013-02-22 15:41:12 -05:00
type : 'POST' ,
url : "/t/" + ( this . get ( 'id' ) ) + "/invite" ,
data : {
user : user
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
} ) ;
} ,
// Delete this topic
"delete" : function ( callback ) {
2013-03-05 15:39:21 -05:00
return $ . ajax ( "/t/" + ( this . get ( 'id' ) ) , {
2013-02-22 15:41:12 -05:00
type : 'DELETE' ,
success : function ( ) {
return typeof callback === "function" ? callback ( ) : void 0 ;
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
} ) ;
} ,
// Load the posts for this topic
loadPosts : function ( opts ) {
2013-02-25 18:38:11 -05:00
var topic = this ;
if ( ! opts ) opts = { } ;
2013-02-20 13:15:50 -05:00
2013-02-22 15:41:12 -05:00
// Load the first post by default
2013-02-25 18:38:11 -05:00
if ( ( ! opts . bestOf ) && ( ! opts . nearPost ) ) opts . nearPost = 1 ;
2013-02-22 15:41:12 -05:00
// If we already have that post in the DOM, jump to it
if ( Discourse . TopicView . scrollTo ( this . get ( 'id' ) , opts . nearPost ) ) return ;
2013-02-20 13:15:50 -05:00
2013-02-25 18:38:11 -05:00
// If loading the topic succeeded...
var afterTopicLoaded = function ( result ) {
var closestPostNumber , lastPost , postDiff ;
2013-02-22 15:41:12 -05:00
// Update the slug if different
2013-02-25 18:38:11 -05:00
if ( result . slug ) topic . set ( 'slug' , result . slug ) ;
2013-02-22 15:41:12 -05:00
// If we want to scroll to a post that doesn't exist, just pop them to the closest
// one instead. This is likely happening due to a deleted post.
opts . nearPost = parseInt ( opts . nearPost , 10 ) ;
closestPostNumber = 0 ;
postDiff = Number . MAX _VALUE ;
result . posts . each ( function ( p ) {
2013-02-25 18:38:11 -05:00
var diff = Math . abs ( p . post _number - opts . nearPost ) ;
2013-02-22 15:41:12 -05:00
if ( diff < postDiff ) {
postDiff = diff ;
closestPostNumber = p . post _number ;
2013-02-25 18:38:11 -05:00
if ( diff === 0 ) return false ;
2013-02-20 13:15:50 -05:00
}
} ) ;
2013-02-22 15:41:12 -05:00
opts . nearPost = closestPostNumber ;
2013-02-25 18:38:11 -05:00
if ( topic . get ( 'participants' ) ) {
topic . get ( 'participants' ) . clear ( ) ;
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
if ( result . suggested _topics ) {
2013-02-25 18:38:11 -05:00
topic . set ( 'suggested_topics' , Em . A ( ) ) ;
2013-02-22 15:41:12 -05:00
}
2013-02-25 18:38:11 -05:00
topic . mergeAttributes ( result , { suggested _topics : Discourse . Topic } ) ;
topic . set ( 'posts' , Em . A ( ) ) ;
2013-02-22 15:41:12 -05:00
if ( opts . trackVisit && result . draft && result . draft . length > 0 ) {
Discourse . openComposer ( {
draft : Discourse . Draft . getLocal ( result . draft _key , result . draft ) ,
draftKey : result . draft _key ,
draftSequence : result . draft _sequence ,
2013-02-25 18:38:11 -05:00
topic : topic ,
2013-02-22 15:41:12 -05:00
ignoreIfChanged : true
} ) ;
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
// Okay this is weird, but let's store the length of the next post when there
lastPost = null ;
result . posts . each ( function ( p ) {
var post ;
p . scrollToAfterInsert = opts . nearPost ;
post = Discourse . Post . create ( p ) ;
2013-02-25 18:38:11 -05:00
post . set ( 'topic' , topic ) ;
topic . get ( 'posts' ) . pushObject ( post ) ;
2013-02-22 15:41:12 -05:00
lastPost = post ;
} ) ;
2013-02-25 18:38:11 -05:00
topic . set ( 'loaded' , true ) ;
}
var errorLoadingTopic = function ( result ) {
topic . set ( 'errorLoading' , true ) ;
// If the result was 404 the post is not found
2013-02-25 18:43:45 -05:00
if ( result . status === 404 ) {
2013-02-25 18:38:11 -05:00
topic . set ( 'errorTitle' , Em . String . i18n ( 'topic.not_found.title' ) )
topic . set ( 'message' , Em . String . i18n ( 'topic.not_found.description' ) ) ;
return ;
}
// If the result is 403 it means invalid access
2013-02-25 18:43:45 -05:00
if ( result . status === 403 ) {
2013-02-25 18:38:11 -05:00
topic . set ( 'errorTitle' , Em . String . i18n ( 'topic.invalid_access.title' ) )
topic . set ( 'message' , Em . String . i18n ( 'topic.invalid_access.description' ) ) ;
return ;
}
// Otherwise supply a generic error message
topic . set ( 'errorTitle' , Em . String . i18n ( 'topic.server_error.title' ) )
topic . set ( 'message' , Em . String . i18n ( 'topic.server_error.description' ) ) ;
}
// Finally, call our find method
Discourse . Topic . find ( this . get ( 'id' ) , {
nearPost : opts . nearPost ,
bestOf : opts . bestOf ,
trackVisit : opts . trackVisit
} ) . then ( afterTopicLoaded , errorLoadingTopic ) ;
2013-02-22 15:41:12 -05:00
} ,
notificationReasonText : ( function ( ) {
var locale _string ;
locale _string = "topic.notifications.reasons." + this . notification _level ;
if ( typeof this . notifications _reason _id === 'number' ) {
locale _string += "_" + this . notifications _reason _id ;
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
return Em . String . i18n ( locale _string , { username : Discourse . currentUser . username . toLowerCase ( ) } ) ;
} ) . property ( 'notifications_reason_id' ) ,
updateNotifications : function ( v ) {
this . set ( 'notification_level' , v ) ;
this . set ( 'notifications_reason_id' , null ) ;
2013-03-05 15:39:21 -05:00
return $ . ajax ( {
2013-02-22 15:41:12 -05:00
url : "/t/" + ( this . get ( 'id' ) ) + "/notifications" ,
type : 'POST' ,
data : {
notification _level : v
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
} ) ;
} ,
// use to add post to topics protecting from dupes
pushPosts : function ( newPosts ) {
var map , posts ;
map = { } ;
posts = this . get ( 'posts' ) ;
posts . each ( function ( p ) {
map [ "" + p . post _number ] = true ;
} ) ;
return newPosts . each ( function ( p ) {
if ( ! map [ p . get ( 'post_number' ) ] ) {
return posts . pushObject ( p ) ;
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
} ) ;
} ,
2013-03-06 15:17:07 -05:00
/ * *
Clears the pin from a topic for the currentUser
@ method clearPin
* * /
clearPin : function ( ) {
var topic = this ;
// Clear the pin optimistically from the object
topic . set ( 'pinned' , false ) ;
$ . ajax ( "/t/" + this . get ( 'id' ) + "/clear-pin" , {
type : 'PUT' ,
error : function ( ) {
// On error, put the pin back
topic . set ( 'pinned' , true ) ;
}
} ) ;
} ,
2013-02-22 15:41:12 -05:00
// Is the reply to a post directly below it?
isReplyDirectlyBelow : function ( post ) {
var postBelow , posts ;
posts = this . get ( 'posts' ) ;
2013-02-25 18:38:11 -05:00
if ( ! posts ) return ;
2013-02-22 15:41:12 -05:00
postBelow = posts [ posts . indexOf ( post ) + 1 ] ;
2013-02-25 18:38:11 -05:00
2013-02-22 15:41:12 -05:00
// 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' ) : void 0 ) === post . get ( 'post_number' ) ;
}
} ) ;
2013-03-05 14:52:35 -05:00
Discourse . Topic . reopenClass ( {
2013-02-22 15:41:12 -05:00
NotificationLevel : {
WATCHING : 3 ,
TRACKING : 2 ,
REGULAR : 1 ,
MUTE : 0
} ,
// Load a topic, but accepts a set of filters
// options:
// onLoad - the callback after the topic is loaded
find : function ( topicId , opts ) {
2013-02-25 18:38:11 -05:00
var data , promise , url ;
2013-02-22 15:41:12 -05:00
url = "/t/" + topicId ;
2013-02-25 18:38:11 -05:00
2013-02-22 15:41:12 -05:00
if ( opts . nearPost ) {
url += "/" + opts . nearPost ;
}
2013-02-25 18:38:11 -05:00
2013-02-22 15:41:12 -05:00
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 ) {
return data . username _filters . push ( username ) ;
} ) ;
}
// Add the best of filter if we have it
if ( opts . bestOf === true ) {
data . best _of = true ;
}
// Check the preload store. If not, load it via JSON
promise = new RSVP . Promise ( ) ;
PreloadStore . get ( "topic_" + topicId , function ( ) {
2013-03-05 15:39:21 -05:00
return $ . getJSON ( url + ".json" , data ) ;
2013-02-22 15:41:12 -05:00
} ) . then ( function ( result ) {
var first ;
first = result . posts . first ( ) ;
if ( first && opts && opts . bestOf ) {
first . bestOfFirst = true ;
2013-02-20 13:15:50 -05:00
}
2013-02-22 15:41:12 -05:00
return promise . resolve ( result ) ;
} , function ( result ) {
return promise . reject ( result ) ;
} ) ;
return promise ;
} ,
// Create a topic from posts
movePosts : function ( topicId , title , postIds ) {
2013-03-05 15:39:21 -05:00
return $ . ajax ( "/t/" + topicId + "/move-posts" , {
2013-02-22 15:41:12 -05:00
type : 'POST' ,
2013-02-25 18:38:11 -05:00
data : { title : title , post _ids : postIds }
2013-02-22 15:41:12 -05:00
} ) ;
} ,
create : function ( obj , topicView ) {
return Object . tap ( this . _super ( obj ) , function ( result ) {
if ( result . participants ) {
result . participants = result . participants . map ( function ( u ) {
return Discourse . User . create ( u ) ;
} ) ;
result . fewParticipants = Em . A ( ) ;
return result . participants . each ( function ( p ) {
2013-02-25 18:38:11 -05:00
if ( result . fewParticipants . length >= 8 ) return false ;
2013-02-22 15:41:12 -05:00
result . fewParticipants . pushObject ( p ) ;
return true ;
2013-02-20 13:15:50 -05:00
} ) ;
}
2013-02-22 15:41:12 -05:00
} ) ;
}
} ) ;
2013-02-20 13:15:50 -05:00