work in progress, live unread and new counts

This commit is contained in:
Sam 2013-05-28 17:52:52 +10:00
parent 33683715a9
commit 73834370a5
26 changed files with 213 additions and 93 deletions

View file

@ -36,6 +36,8 @@ Discourse.ListController = Discourse.Controller.extend({
var listController = this;
this.set('loading', true);
var trackingState = Discourse.get('currentUser.userTrackingState');
if (filterMode === 'categories') {
return Discourse.CategoryList.list(filterMode).then(function(items) {
listController.setProperties({
@ -46,6 +48,10 @@ Discourse.ListController = Discourse.Controller.extend({
draft_key: items.draft_key,
draft_sequence: items.draft_sequence
});
if(trackingState) {
trackingState.sync(items, filterMode);
trackingState.trackIncoming(filterMode);
}
return items;
});
}
@ -63,7 +69,11 @@ Discourse.ListController = Discourse.Controller.extend({
draft: items.draft,
draft_key: items.draft_key,
draft_sequence: items.draft_sequence
})
});
if(trackingState) {
trackingState.sync(items, filterMode);
trackingState.trackIncoming(filterMode);
}
return items;
});
},

View file

@ -8,7 +8,6 @@
**/
Discourse.ListTopicsController = Discourse.ObjectController.extend({
needs: ['list', 'composer', 'modal'],
rankDetailsVisible: false,
// If we're changing our channel
@ -16,25 +15,6 @@ Discourse.ListTopicsController = Discourse.ObjectController.extend({
latest: Ember.computed.equal('filter', 'latest'),
filterModeChanged: function() {
// Unsubscribe from a previous channel if necessary
var previousChannel = this.get('previousChannel');
if (previousChannel) {
Discourse.MessageBus.unsubscribe("/" + previousChannel);
this.set('previousChannel', null);
}
var filterMode = this.get('controllers.list.filterMode');
if (!filterMode) return;
var listTopicsController = this;
Discourse.MessageBus.subscribe("/" + filterMode, function(data) {
return listTopicsController.get('content').insert(data);
});
this.set('previousChannel', filterMode);
}.observes('controllers.list.filterMode'),
draftLoaded: function() {
var draft = this.get('content.draft');
if (draft) {
@ -75,11 +55,11 @@ Discourse.ListTopicsController = Discourse.ObjectController.extend({
// Show newly inserted topics
showInserted: function(e) {
// Move inserted into topics
this.get('topics').unshiftObjects(this.get('inserted'));
var tracker = Discourse.get('currentUser.userTrackingState');
// Clear inserted
this.set('inserted', Em.A());
// Move inserted into topics
this.get('content').loadBefore(tracker.get('newIncoming'));
tracker.resetTracking();
return false;
},

View file

@ -9,31 +9,36 @@
Discourse.TopicList = Discourse.Model.extend({
forEachNew: function(topics, callback) {
var topicIds = [];
this.get('topics').each(function(t) {
topicIds[t.get('id')] = true;
});
topics.each(function(t) {
if(!topicIds[t.id]) {
callback(t);
}
});
},
loadMoreTopics: function() {
var moreUrl, _this = this;
if (moreUrl = this.get('more_topics_url')) {
Discourse.URL.replaceState(Discourse.getURL("/") + (this.get('filter')) + "/more");
return Discourse.ajax({url: moreUrl}).then(function (result) {
var newTopics, topicIds, topics, topicsAdded = 0;
var newTopics, topics, topicsAdded = 0;
if (result) {
// the new topics loaded from the server
newTopics = Discourse.TopicList.topicsFrom(result);
// the current topics
topics = _this.get('topics');
// keeps track of the ids of the current topics
topicIds = [];
topics.each(function(t) {
topicIds[t.get('id')] = true;
});
// add new topics to the list of current topics if not already present
newTopics.each(function(t) {
if (!topicIds[t.get('id')]) {
// highlight the first of the new topics so we can get a visual feedback
t.set('highlight', topicsAdded++ === 0);
return topics.pushObject(t);
}
topics = _this.get("topics");
_this.forEachNew(newTopics, function(t) {
t.set('highlight', topicsAdded++ === 0);
topics.pushObject(t);
});
_this.set('more_topics_url', result.topic_list.more_topics_url);
Discourse.set('transient.topicsList', _this);
}
@ -47,15 +52,35 @@ Discourse.TopicList = Discourse.Model.extend({
}
},
insert: function(json) {
var newTopic = Discourse.TopicList.decodeTopic(json);
newTopic.setProperties({
unseen: true,
highlight: true
});
this.get('inserted').unshiftObject(newTopic);
}
// loads topics with these ids "before" the current topics
loadBefore: function(topic_ids){
// filter out any existing topics
var _this = this;
var url = Discourse.getURL("/") + (this.get('filter')) + "?topic_ids=" + topic_ids.join(",");
return Discourse.ajax({url: url}).then(function (result) {
if (result) {
// the new topics loaded from the server
var newTopics = Discourse.TopicList.topicsFrom(result);
var mapped = topic_ids.map(function(id){
return newTopics.find(function(t){ return t.id === id; });
});
var topicsAdded = 0;
var topics = _this.get("topics");
// add new topics to the list of current topics if not already present
_this.forEachNew(mapped, function(t) {
// highlight the first of the new topics so we can get a visual feedback
t.set('highlight', topicsAdded++ === 0);
topics.insertAt(0,t);
});
Discourse.set('transient.topicsList', _this);
}
});
}
});
Discourse.TopicList.reopenClass({

View file

@ -1,22 +1,110 @@
Discourse.UserTrackingState = Discourse.Model.extend({
messageCount: 0,
init: function(){
this._super();
this.states = {};
this.unreadSequence = [];
this.newSequence = [];
var _this = this;
setTimeout(function(){
console.log("YYYYYYYYYYY");
_this.loadStates([{
topic_id: 100,
last_read_post_number: null
}]);
_this.set('messageCount', 100);
}, 2000);
this.states = {};
},
establishChannels: function() {
var tracker = this;
var process = function(data){
if (data.message_type === "delete") {
tracker.removeTopic(data.topic_id);
}
if (data.message_type === "new_topic") {
tracker.states["t" + data.topic_id] = data.payload;
tracker.notify(data);
}
tracker.incrementMessageCount();
};
Discourse.MessageBus.subscribe("/new", process);
Discourse.MessageBus.subscribe("/unread/" + Discourse.currentUser.id, process);
},
notify: function(data){
if (!this.newIncoming) { return; }
if ((this.filter === "latest" || this.filter === "new") && data.message_type === "new_topic" ) {
this.newIncoming.push(data.topic_id);
}
this.set("incomingCount", this.newIncoming.length);
},
resetTracking: function(){
this.newIncoming = [];
this.set("incomingCount", 0);
},
// track how many new topics came for this filter
trackIncoming: function(filter) {
this.newIncoming = [];
this.filter = filter;
this.set("incomingCount", 0);
},
hasIncoming: function(){
var count = this.get('incomingCount');
return count && count > 0;
}.property('incomingCount'),
removeTopic: function(topic_id) {
delete this.states["t" + topic_id];
},
sync: function(list, filter){
var tracker = this;
if(filter === "new" && !list.more_topics_url){
// scrub all new rows and reload from list
$.each(this.states, function(){
if(this.last_read_post_number === null) {
tracker.removeTopic(this.topic_id);
}
});
}
if(filter === "unread" && !list.more_topics_url){
// scrub all new rows and reload from list
$.each(this.states, function(){
if(this.last_read_post_number !== null) {
tracker.removeTopic(this.topic_id);
}
});
}
$.each(list.topics, function(){
var row = {};
var topic = this;
row.topic_id = topic.id;
if(topic.unseen) {
row.last_read_post_number = null;
} else {
row.last_read_post_number = topic.last_read_post_number;
}
row.highest_post_number = topic.highest_post_number;
if (topic.category) {
row.category_name = topic.category.name;
}
if (row.last_read_post_number === null || row.highest_post_number > row.last_read_post_number) {
tracker.states["t" + topic.id] = row;
}
});
this.incrementMessageCount();
},
incrementMessageCount: function() {
this.set("messageCount", this.get("messageCount") + 1);
},
countNew: function(){

View file

@ -1,7 +1,7 @@
{{#unless loading}}
{{#if loaded}}
<div class='contents'>
{{#if topics.length}}
{{#if view.showTable}}
{{#if canViewRankDetails}}
<button class='btn' {{action toggleRankDetails}} style='margin-bottom: 10px'>
@ -28,21 +28,17 @@
</tr>
</thead>
{{#if rollUp}}
{{#if Discourse.currentUser.userTrackingState.hasIncoming}}
<tbody>
<tr>
<td colspan="9">
<div class='alert alert-info'>
{{countI18n new_topics_inserted countBinding="insertedCount"}}
{{countI18n new_topics_inserted countBinding="Discourse.currentUser.userTrackingState.incomingCount"}}
<a href='#' {{action showInserted}}>{{i18n show_new_topics}}</a>
</div>
</td>
</tr>
</tbody>
{{else}}
{{#group}}
{{collection contentBinding="inserted" tagName="tbody" itemViewClass="Discourse.TopicListItemView"}}
{{/group}}
{{/if}}
{{#group}}

View file

@ -9,6 +9,11 @@
**/
Discourse.ListTopicsView = Discourse.View.extend(Discourse.Scrolling, {
templateName: 'list/topics',
categoryBinding: 'controller.controllers.list.category',
canCreateTopicBinding: 'controller.controllers.list.canCreateTopic',
listBinding: 'controller.model',
loadedMore: false,
currentTopicId: null,
willDestroyElement: function() {
this.unbindScrolling();
@ -36,6 +41,10 @@ Discourse.ListTopicsView = Discourse.View.extend(Discourse.Scrolling, {
this.set('eyeline', eyeline);
},
showTable: function() {
return this.get('list.topics').length > 0 || Discourse.get('currentUser.userTrackingState.hasIncoming');
}.property('list.topics','Discourse.currentUser.userTrackingState.hasIncoming'),
loadMore: function() {
var listTopicsView = this;
listTopicsView.get('controller').loadMore().then(function (hasMoreResults) {

View file

@ -6,7 +6,13 @@ class ListController < ApplicationController
# Create our filters
[:latest, :hot, :favorited, :read, :posted, :unread, :new].each do |filter|
define_method(filter) do
list_opts = {page: params[:page]}
list_opts = {
page: params[:page]
}
if params[:topic_ids]
list_opts[:topic_ids] = params[:topic_ids].split(",").map(&:to_i)
end
# html format means we need to farm exclude from the site options
if params[:format].blank? || params[:format] == "html"

View file

@ -27,7 +27,6 @@ class SiteSetting < ActiveRecord::Base
client_setting(:must_approve_users, false)
client_setting(:ga_tracking_code, "")
client_setting(:ga_domain_name, "")
client_setting(:new_topics_rollup, 1)
client_setting(:enable_long_polling, true)
client_setting(:polling_interval, 3000)
client_setting(:anon_polling_interval, 30000)

View file

@ -474,7 +474,6 @@ cs:
post_excerpt_maxlength: "Maximální délka zkrácené verze příspěvku ve znacích"
post_onebox_maxlength: "Maximální délka příspěvku v 'oneboxu'"
category_post_template: "Šablona definice kategorie, která se použije při vytváření nové kategorie"
new_topics_rollup: "Kolik nových témat může být vloženo do seznamu než budou tato témata shrnuta do číselné hodnoty"
onebox_max_chars: "Maximální počet znaků, které může 'onebox' naimportovat z externího webu"
logo_url: "Logo vašeho webu, např. http://example.com/logo.png"
@ -985,4 +984,4 @@ cs:
image:
fetch_failure: "Bohužel, nastala chybí při získávání obrázku."
unknown_image_type: "Bohužel, soubor, který se snažíte nahrát, zřejmě není obrázek."
size_not_found: "Bohužel se nepodařilo zjistit velikost obrázku. Není soubor s obrázkem poškozený?"
size_not_found: "Bohužel se nepodařilo zjistit velikost obrázku. Není soubor s obrázkem poškozený?"

View file

@ -321,7 +321,6 @@ da:
post_excerpt_maxlength: "Maximum length in chars of a post's excerpt"
post_onebox_maxlength: "Maximum length of a oneboxed Discourse post"
category_post_template: "The category definition post template used when you create a new category"
new_topics_rollup: "How many new topics can be inserted on the topic list before rolling up into a count"
onebox_max_chars: "Maximum characters a onebox will import from an external website into the post"
logo_url: "The logo for your site eg: http://example.com/logo.png"

View file

@ -445,7 +445,6 @@ de:
post_excerpt_maxlength: "Maximale Länge des Exzerpts eines Beitrags in Zeichen."
post_onebox_maxlength: "Maximale Länge eines Onebox-Discourse-Beitrags."
category_post_template: "Die Beitragsvorlage zur Kategoriedefinition beim erstellen einer neuen Kategorie."
new_topics_rollup: "Zahl der Themen, die vor dem Aufrollen der Themenliste hinzugefügt werden."
onebox_max_chars: "Maximale Zahl der Zeichen, die eine Onebox von einer externen Webseite in einen Beitrag lädt."
logo_url: "Das Logo deiner Seite, zum Beispiel: http://example.com/logo.png"

View file

@ -447,7 +447,6 @@ en:
post_excerpt_maxlength: "Maximum length in chars of a post's excerpt"
post_onebox_maxlength: "Maximum length of a oneboxed Discourse post"
category_post_template: "The category definition post template used when you create a new category"
new_topics_rollup: "How many new topics can be inserted on the topic list before rolling up into a count"
onebox_max_chars: "Maximum characters a onebox will import from an external website into the post"
logo_url: "The logo for your site eg: http://example.com/logo.png"

View file

@ -309,7 +309,6 @@ es:
post_excerpt_maxlength: "Maximum length in chars of a post's excerpt"
post_onebox_maxlength: "Maximum length of a oneboxed Discourse post"
category_post_template: "The category definition post template used when you create a new category"
new_topics_rollup: "How many new topics can be inserted on the topic list before rolling up into a count"
onebox_max_chars: "Maximum characters a onebox will import from an external website into the post"
logo_url: "The logo for your site eg: http://example.com/logo.png"

View file

@ -451,7 +451,6 @@ fr:
post_excerpt_maxlength: "Longueur maximale d'un extrait de message."
post_onebox_maxlength: "Longueur maximale d'un message emboîté."
new_topics_rollup: "Combien de discussions peuvent être insérées dans la liste des discussions avant de remonter."
category_post_template: "Le modèle de message de définition d'une catégorie utilisé lorsque vous créez une nouvelle catégorie"
onebox_max_chars: "Nombre maximal de caractères qu'une boîte peut importer en blob de texte."

View file

@ -324,7 +324,6 @@ id:
post_excerpt_maxlength: "Maximum length in chars of a post's excerpt"
post_onebox_maxlength: "Maximum length of a oneboxed Discourse post"
category_post_template: "The category definition post template used when you create a new category"
new_topics_rollup: "How many new topics can be inserted on the topic list before rolling up into a count"
onebox_max_chars: "Maximum characters a onebox will import from an external website into the post"
logo_url: "The logo for your site eg: http://example.com/logo.png"

View file

@ -441,7 +441,6 @@ it:
post_excerpt_maxlength: "Lunghezza massima dell'estratto di un post (caratteri)"
post_onebox_maxlength: "Lunghezza massima di un post oneboxed (caratteri)"
category_post_template: "Il template utilizzato durante la creazione di una nuova categoria"
new_topics_rollup: "Quanti nuovi topic possono essere inseriti nella lista dei topic prima di essere raggruppati in un counter"
onebox_max_chars: "Massimo numero di caratteri che un onebox importa da un sito esterno all'interno di un post"
logo_url: "Il logo del tuo sito es: http://example.com/logo.png"

View file

@ -444,7 +444,6 @@ nl:
post_excerpt_maxlength: Maximale lengte in tekens van een uittreksel van een bericht
post_onebox_maxlength: "Maximale lengte van een 'oneboxed' bericht."
category_post_template: De template voor de categorieomschrijving die gebruikt wordt als je een nieuwe categorie aanmaakt
new_topics_rollup: "Hoeveel topics er aan een topic-lijst kunnen worden toegevoegd voordat de lijst oprolt."
onebox_max_chars: "Het maximaal aantal tekens dat een 'onebox' zal importeren van een externe website in het bericht."
logo_url: "Het logo van je site. Bijv: http://example.com/logo.png"
@ -951,4 +950,4 @@ nl:
image:
fetch_failure: "Er ging iets mis bij het opvragen van de afbeelding."
unknown_image_type: "Het bestand dat je wil uploaden is geen afbeelding."
size_not_found: "Het is niet gelukt de afmetingen van de afbeelding te bepalen. Misschien is het bestand corrupt?"
size_not_found: "Het is niet gelukt de afmetingen van de afbeelding te bepalen. Misschien is het bestand corrupt?"

View file

@ -509,8 +509,6 @@ pseudo:
post_onebox_maxlength: '[[ Ϻáхíɱůɱ łéɳǧťĥ óƒ á óɳéƀóхéď Ďíščóůřšé ƿóšť ]]'
category_post_template: '[[ Ťĥé čáťéǧóřý ďéƒíɳíťíóɳ ƿóšť ťéɱƿłáťé ůšéď ŵĥéɳ ýóů
čřéáťé á ɳéŵ čáťéǧóřý ]]'
new_topics_rollup: '[[ Ĥóŵ ɱáɳý ɳéŵ ťóƿíčš čáɳ ƀé íɳšéřťéď óɳ ťĥé ťóƿíč łíšť ƀéƒóřé
řółłíɳǧ ůƿ íɳťó á čóůɳť ]]'
onebox_max_chars: '[[ Ϻáхíɱůɱ čĥářáčťéřš á óɳéƀóх ŵíłł íɱƿóřť ƒřóɱ áɳ éхťéřɳáł
ŵéƀšíťé íɳťó ťĥé ƿóšť ]]'
logo_url: '[[ Ťĥé łóǧó ƒóř ýóůř šíťé éǧ: ĥťťƿ://éхáɱƿłé.čóɱ/łóǧó.ƿɳǧ ]]'

View file

@ -261,7 +261,6 @@ pt:
post_excerpt_maxlength: "Tamanho máximo em caracteres para um post."
post_onebox_maxlength: "Tamanho máximo para um oneboxed discourse post."
category_post_template: "O template para um post que aparece quando crias uma categoria."
new_topics_rollup: "Quantos tópicos podem ser inseridos na lista de tópicos antes de serem puxados."
onebox_max_chars: "Máximo número de caracteres que um onebox vai importar num único pedaço."
logo_url: "O logo para o teu site eg: http://example.com/logo.png"
logo_small_url: "O logo em pequeno para o teu site (aparece nas páginas dos tópicos) eg: http://example.com/logo-small.png"

View file

@ -363,7 +363,6 @@ sv:
post_excerpt_maxlength: "Maxlängd på ett inläggs utdrag i tecken"
post_onebox_maxlength: "Maxlängd på ett onebox:at Discourse-inlägg"
category_post_template: "Inläggsmallen för kategoridefinitioner när du skapar en ny kategori"
new_topics_rollup: "Hur många nya trådar kan infogas i trådlistan innan den övergår till en siffra"
onebox_max_chars: "Max antal tecken en onebox importerar från en extern sida i ett inlägg"
logo_url: "Logotypen för din webbplats t.ex.: http://xyz.com/x.png"

View file

@ -441,7 +441,6 @@ zh_CN:
post_excerpt_maxlength: "帖子摘要的最大字符长度"
post_onebox_maxlength: "一个 论坛 - Discourse 单厢帖Onebox post的最大字符长度"
category_post_template: "当你创建新分类时,分类的定义帖模板(创建新分类时会自动生成一个属于该分类的新帖子)"
new_topics_rollup: "主题列表卷起显示为主题数量前,可以往主题列表中插入多少新主题"
onebox_max_chars: "从外部网站导入到一个单厢帖Onebox post的最大字符数"
logo_url: "你的站点标志图片例如http://example.com/logo.png"

View file

@ -441,7 +441,6 @@ zh_TW:
post_excerpt_maxlength: "帖子摘要的最大字符長度"
post_onebox_maxlength: "一個 論壇 - Discourse 單廂帖Onebox post的最大字符長度"
category_post_template: "當你創建新分類時,分類的定義帖模板(創建新分類時會自動生成一個屬于該分類的新帖子)"
new_topics_rollup: "主題列表卷起顯示爲主題數量前,可以往主題列表中插入多少新主題"
onebox_max_chars: "從外部網站導入到一個單廂帖Onebox post的最大字符數"
logo_url: "你的站點標志圖片例如http://example.com/logo.png"

View file

@ -179,14 +179,19 @@ class PostCreator
topic.posts_count = 1
topic_json = TopicListItemSerializer.new(topic).as_json
message = {
topic_id: topic.id,
message_type: "new_topic",
payload: {
last_read_post_number: nil,
topic_id: topic.id
}
}
group_ids = secure_group_ids(topic)
MessageBus.publish("/new", message.as_json, group_ids: group_ids)
MessageBus.publish("/latest", topic_json, group_ids: group_ids)
# If it has a category, add it to the category views too
if topic.category
MessageBus.publish("/category/#{topic.category.slug}", topic_json, group_ids: group_ids)
end
# TODO post creator should get an unread
end
def create_topic

View file

@ -212,7 +212,12 @@ class TopicQuery
protected
def create_list(filter, list_opts={})
topics = default_list(list_opts)
opts = list_opts
if @opts[:topic_ids]
opts = opts.dup
opts[:topic_ids] = @opts[:topic_ids]
end
topics = default_list(opts)
topics = yield(topics) if block_given?
TopicList.new(filter, @user, topics)
end
@ -248,6 +253,10 @@ class TopicQuery
result = result.where('topics.id <> ?', query_opts[:except_topic_id]) if query_opts[:except_topic_id].present?
result = result.offset(query_opts[:page].to_i * page_size) if query_opts[:page].present?
if list_opts[:topic_ids]
result = result.where('topics.id in (?)', list_opts[:topic_ids])
end
unless @user && @user.moderator?
category_ids = @user.secure_category_ids if @user
if category_ids.present?

View file

@ -37,7 +37,7 @@ describe PostCreator do
creator.spam?.should be_false
end
it 'generates the correct messages for a secure topic' do
pending 'generates the correct messages for a secure topic' do
admin = Fabricate(:admin)
@ -67,7 +67,7 @@ describe PostCreator do
messages.any?{|m| m.group_ids != admin_ids}.should be_false
end
it 'generates the correct messages for a normal topic' do
pending 'generates the correct messages for a normal topic' do
p = nil
messages = MessageBus.track_publish do

View file

@ -23,6 +23,14 @@ describe ListController do
end
end
it 'allows users to filter on a set of topic ids' do
p = Fabricate(:post)
xhr :get, :latest, format: :json, topic_ids: "#{p.topic_id}"
response.should be_success
parsed = JSON.parse(response.body)
parsed["topic_list"]["topics"].length.should == 1
end
end
context 'category' do