mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-30 19:08:10 -05:00
refactor analysis of read posts and centralize logic
This commit is contained in:
parent
a73c9dd530
commit
e99f137316
4 changed files with 154 additions and 146 deletions
|
@ -14,7 +14,91 @@
|
||||||
**/
|
**/
|
||||||
Discourse.Eyeline = function Eyeline(selector) {
|
Discourse.Eyeline = function Eyeline(selector) {
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Call this to analyze the positions of all the nodes in a set
|
||||||
|
|
||||||
|
returns: a hash with top, bottom and onScreen items
|
||||||
|
{top: , bottom:, onScreen:}
|
||||||
|
**/
|
||||||
|
Discourse.Eyeline.analyze = function(rows) {
|
||||||
|
var current, goingUp, i, increment, offset,
|
||||||
|
winHeight, winOffset, detected, onScreen,
|
||||||
|
bottom, top, outerHeight;
|
||||||
|
|
||||||
|
if (rows.length === 0) return;
|
||||||
|
|
||||||
|
i = parseInt(rows.length / 2, 10);
|
||||||
|
increment = parseInt(rows.length / 4, 10);
|
||||||
|
goingUp = undefined;
|
||||||
|
winOffset = window.pageYOffset || $('html').scrollTop();
|
||||||
|
winHeight = window.innerHeight || $(window).height();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (i === 0 || (i >= rows.length - 1)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = $(rows[i]);
|
||||||
|
offset = current.offset();
|
||||||
|
|
||||||
|
if (offset.top - winHeight < winOffset) {
|
||||||
|
if (offset.top + current.outerHeight() - window.innerHeight > winOffset) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
i = i + increment;
|
||||||
|
if (goingUp !== undefined && increment === 1 && !goingUp) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
goingUp = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i = i - increment;
|
||||||
|
if (goingUp !== undefined && increment === 1 && goingUp) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
goingUp = false;
|
||||||
|
}
|
||||||
|
if (increment > 1) {
|
||||||
|
increment = parseInt(increment / 2, 10);
|
||||||
|
goingUp = undefined;
|
||||||
|
}
|
||||||
|
if (increment === 0) {
|
||||||
|
increment = 1;
|
||||||
|
goingUp = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onScreen = [];
|
||||||
|
bottom = i;
|
||||||
|
// quick analysis of whats on screen
|
||||||
|
while(true) {
|
||||||
|
if(i < 0) { break;}
|
||||||
|
|
||||||
|
current = $(rows[i]);
|
||||||
|
offset = current.offset();
|
||||||
|
outerHeight = current.outerHeight();
|
||||||
|
|
||||||
|
// on screen
|
||||||
|
if(offset.top > winOffset && offset.top + outerHeight < winOffset + winHeight) {
|
||||||
|
onScreen.unshift(i);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if(offset.top < winOffset) {
|
||||||
|
top = i;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i -=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return({top: top, bottom: bottom, onScreen: onScreen});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Call this whenever you want to consider what is being seen by the browser
|
Call this whenever you want to consider what is being seen by the browser
|
||||||
|
@ -25,9 +109,6 @@ Discourse.Eyeline.prototype.update = function() {
|
||||||
var $elements, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight,
|
var $elements, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight,
|
||||||
_this = this;
|
_this = this;
|
||||||
|
|
||||||
// before anything ... let us not do anything if we have no focus
|
|
||||||
if (!Discourse.get('hasFocus')) { return; }
|
|
||||||
|
|
||||||
docViewTop = $(window).scrollTop();
|
docViewTop = $(window).scrollTop();
|
||||||
windowHeight = $(window).height();
|
windowHeight = $(window).height();
|
||||||
docViewBottom = docViewTop + windowHeight;
|
docViewBottom = docViewTop + windowHeight;
|
||||||
|
|
|
@ -138,6 +138,9 @@ Discourse.ScreenTrack = Ember.Object.extend({
|
||||||
this.topicTime += diff;
|
this.topicTime += diff;
|
||||||
docViewTop = $(window).scrollTop() + $('header').height();
|
docViewTop = $(window).scrollTop() + $('header').height();
|
||||||
docViewBottom = docViewTop + $(window).height();
|
docViewBottom = docViewTop + $(window).height();
|
||||||
|
|
||||||
|
// TODO: Eyeline has a smarter more accurate function here
|
||||||
|
|
||||||
return Object.keys(this.timings, function(id) {
|
return Object.keys(this.timings, function(id) {
|
||||||
var $element, elemBottom, elemTop, timing;
|
var $element, elemBottom, elemTop, timing;
|
||||||
$element = $(id);
|
$element = $(id);
|
||||||
|
|
|
@ -10,16 +10,27 @@
|
||||||
Discourse.Scrolling = Em.Mixin.create({
|
Discourse.Scrolling = Em.Mixin.create({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Begin watching for scroll events. They will be called at max every 100ms.
|
Begin watching for scroll events. By default they will be called at max every 100ms.
|
||||||
|
call with {debounce: N} for a diff time
|
||||||
|
|
||||||
@method bindScrolling
|
@method bindScrolling
|
||||||
*/
|
*/
|
||||||
bindScrolling: function() {
|
bindScrolling: function(opts) {
|
||||||
var onScroll,
|
var onScroll,
|
||||||
_this = this;
|
_this = this;
|
||||||
onScroll = Discourse.debounce(function() { return _this.scrolled(); }, 100);
|
|
||||||
|
opts = opts || {debounce: 100};
|
||||||
|
|
||||||
|
if (opts.debounce) {
|
||||||
|
onScroll = Discourse.debounce(function() { return _this.scrolled(); }, 100);
|
||||||
|
} else {
|
||||||
|
onScroll = function(){ return _this.scrolled(); };
|
||||||
|
}
|
||||||
|
|
||||||
$(document).bind('touchmove.discourse', onScroll);
|
$(document).bind('touchmove.discourse', onScroll);
|
||||||
$(window).bind('scroll.discourse', onScroll);
|
$(window).bind('scroll.discourse', onScroll);
|
||||||
|
|
||||||
|
// resize is should also fire this cause it causes scrolling of sorts
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -36,7 +36,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
progressWidth = ratio * totalWidth;
|
progressWidth = ratio * totalWidth;
|
||||||
bg = $topicProgress.find('.bg');
|
bg = $topicProgress.find('.bg');
|
||||||
bg.stop(true, true);
|
bg.stop(true, true);
|
||||||
currentWidth = bg.width()
|
currentWidth = bg.width();
|
||||||
|
|
||||||
if (currentWidth === totalWidth) {
|
if (currentWidth === totalWidth) {
|
||||||
bg.width(currentWidth - 1);
|
bg.width(currentWidth - 1);
|
||||||
|
@ -61,13 +61,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
if (title) return Discourse.set('title', title);
|
if (title) return Discourse.set('title', title);
|
||||||
}).observes('topic.loaded', 'topic.title'),
|
}).observes('topic.loaded', 'topic.title'),
|
||||||
|
|
||||||
newPostsPresent: (function() {
|
|
||||||
if (this.get('topic.highest_post_number')) {
|
|
||||||
this.updateBar();
|
|
||||||
this.examineRead();
|
|
||||||
}
|
|
||||||
}).observes('topic.highest_post_number'),
|
|
||||||
|
|
||||||
currentPostChanged: (function() {
|
currentPostChanged: (function() {
|
||||||
var current = this.get('controller.currentPost');
|
var current = this.get('controller.currentPost');
|
||||||
|
|
||||||
|
@ -112,6 +105,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
willDestroyElement: function() {
|
willDestroyElement: function() {
|
||||||
var screenTrack, controller;
|
var screenTrack, controller;
|
||||||
this.unbindScrolling();
|
this.unbindScrolling();
|
||||||
|
$(window).unbind('resize.discourse-on-scroll');
|
||||||
|
|
||||||
controller = this.get('controller');
|
controller = this.get('controller');
|
||||||
controller.unsubscribe();
|
controller.unsubscribe();
|
||||||
|
@ -123,22 +117,13 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set('screenTrack', null);
|
this.set('screenTrack', null);
|
||||||
|
|
||||||
$(window).unbind('scroll.discourse-on-scroll');
|
|
||||||
$(document).unbind('touchmove.discourse-on-scroll');
|
|
||||||
$(window).unbind('resize.discourse-on-scroll');
|
|
||||||
|
|
||||||
this.resetExamineDockCache();
|
this.resetExamineDockCache();
|
||||||
},
|
},
|
||||||
|
|
||||||
didInsertElement: function(e) {
|
didInsertElement: function(e) {
|
||||||
var topicView = this;
|
var topicView = this;
|
||||||
var onScroll = Discourse.debounce(function() { return topicView.onScroll(); }, 10);
|
this.bindScrolling({debounce: 0});
|
||||||
|
$(window).bind('resize.discourse-on-scroll', function() { scrolled(false); });
|
||||||
$(window).bind('scroll.discourse-on-scroll', onScroll);
|
|
||||||
$(document).bind('touchmove.discourse-on-scroll', onScroll);
|
|
||||||
$(window).bind('resize.discourse-on-scroll', onScroll);
|
|
||||||
this.bindScrolling();
|
|
||||||
|
|
||||||
var controller = this.get('controller');
|
var controller = this.get('controller');
|
||||||
controller.subscribe();
|
controller.subscribe();
|
||||||
|
@ -151,44 +136,19 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
screenTrack.start();
|
screenTrack.start();
|
||||||
this.set('screenTrack', screenTrack);
|
this.set('screenTrack', screenTrack);
|
||||||
|
|
||||||
// Track the user's eyeline
|
|
||||||
var eyeline = new Discourse.Eyeline('.topic-post');
|
|
||||||
eyeline.on('saw', function(e) {
|
|
||||||
topicView.postSeen(e.detail);
|
|
||||||
});
|
|
||||||
|
|
||||||
eyeline.on('sawBottom', function(e) {
|
|
||||||
topicView.postSeen(e.detail);
|
|
||||||
topicView.nextPage(e.detail);
|
|
||||||
});
|
|
||||||
|
|
||||||
eyeline.on('sawTop', function(e) {
|
|
||||||
topicView.postSeen(e.detail);
|
|
||||||
topicView.prevPage(e.detail);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.set('eyeline', eyeline);
|
|
||||||
this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) {
|
this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) {
|
||||||
return Discourse.ClickTrack.trackClick(e);
|
return Discourse.ClickTrack.trackClick(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onScroll();
|
this.scrolled();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Triggered whenever any posts are rendered, debounced to save over calling
|
// Triggered whenever any posts are rendered, debounced to save over calling
|
||||||
postsRendered: Discourse.debounce(function() {
|
postsRendered: Discourse.debounce(function() {
|
||||||
var $window = $(window);
|
var $window = $(window);
|
||||||
var $lastPost = $('.row:last');
|
var $lastPost = $('.row:last');
|
||||||
|
this.scrolled(false);
|
||||||
// we consider stuff at the end of the list as read, right away (if it is visible)
|
}, 50),
|
||||||
if ($window.height() + $window.scrollTop() >= $lastPost.offset().top + $lastPost.height()) {
|
|
||||||
this.examineRead();
|
|
||||||
} else {
|
|
||||||
// last is not in view, so only examine in 2 seconds
|
|
||||||
var topicView = this;
|
|
||||||
Em.run.later(function() { topicView.examineRead(); }, 2000);
|
|
||||||
}
|
|
||||||
}, 100),
|
|
||||||
|
|
||||||
resetRead: function(e) {
|
resetRead: function(e) {
|
||||||
this.get('screenTrack').cancel();
|
this.get('screenTrack').cancel();
|
||||||
|
@ -204,18 +164,16 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
|
|
||||||
gotFocus: function(){
|
gotFocus: function(){
|
||||||
if (Discourse.get('hasFocus')){
|
if (Discourse.get('hasFocus')){
|
||||||
this.examineRead();
|
this.scrolled();
|
||||||
}
|
}
|
||||||
}.observes("Discourse.hasFocus"),
|
}.observes("Discourse.hasFocus"),
|
||||||
|
|
||||||
// Called for every post seen
|
// Called for every post seen, returns the post number
|
||||||
postSeen: function($post) {
|
postSeen: function($post) {
|
||||||
var post, postView, _ref;
|
var post, postView, _ref;
|
||||||
this.set('postNumberSeen', null);
|
|
||||||
postView = Ember.View.views[$post.prop('id')];
|
postView = Ember.View.views[$post.prop('id')];
|
||||||
if (postView) {
|
if (postView) {
|
||||||
post = postView.get('post');
|
post = postView.get('post');
|
||||||
this.set('postNumberSeen', post.get('post_number'));
|
|
||||||
if (post.get('post_number') > (this.get('topic.last_read_post_number') || 0)) {
|
if (post.get('post_number') > (this.get('topic.last_read_post_number') || 0)) {
|
||||||
this.set('topic.last_read_post_number', post.get('post_number'));
|
this.set('topic.last_read_post_number', post.get('post_number'));
|
||||||
}
|
}
|
||||||
|
@ -224,6 +182,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
_ref = this.get('screenTrack');
|
_ref = this.get('screenTrack');
|
||||||
if (_ref) { _ref.guessedSeen(post.get('post_number')); }
|
if (_ref) { _ref.guessedSeen(post.get('post_number')); }
|
||||||
}
|
}
|
||||||
|
return post.get('post_number');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -309,22 +268,12 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
}).observes('topic.highest_post_number'),
|
}).observes('topic.highest_post_number'),
|
||||||
|
|
||||||
loadMore: function(post) {
|
loadMore: function(post) {
|
||||||
if (this.get('controller.loading') || this.get('controller.seenBottom')) return;
|
if (this.get('controller.loading')) { return; }
|
||||||
|
|
||||||
// Don't load if we know we're at the bottom
|
// Don't load if we know we're at the bottom
|
||||||
if (this.get('topic.highest_post_number') === post.get('post_number')) {
|
if (this.get('topic.highest_post_number') === post.get('post_number')) { return; }
|
||||||
var eyeline = this.get('eyeline');
|
|
||||||
if (eyeline) {
|
|
||||||
eyeline.flushRest();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update our current post to the last number we saw
|
if (this.get('controller.seenBottom')) { return; }
|
||||||
var postNumberSeen = this.get('postNumberSeen');
|
|
||||||
if (postNumberSeen) {
|
|
||||||
this.set('controller.currentPost', postNumberSeen);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't double load ever
|
// Don't double load ever
|
||||||
if (this.topic.posts.last().post_number !== post.post_number) return;
|
if (this.topic.posts.last().post_number !== post.post_number) return;
|
||||||
|
@ -353,26 +302,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Examine which posts are on the screen and mark them as read. Also figure out if we
|
|
||||||
// need to load more posts.
|
|
||||||
examineRead: function() {
|
|
||||||
// Track posts time on screen
|
|
||||||
var postNumberSeen, _ref, _ref1;
|
|
||||||
if (_ref = this.get('screenTrack')) {
|
|
||||||
_ref.scrolled();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update what we can see
|
|
||||||
if (_ref1 = this.get('eyeline')) {
|
|
||||||
_ref1.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update our current post to the last number we saw
|
|
||||||
if (postNumberSeen = this.get('postNumberSeen')) {
|
|
||||||
this.set('controller.currentPost', postNumberSeen);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelEdit: function() {
|
cancelEdit: function() {
|
||||||
// close editing mode
|
// close editing mode
|
||||||
this.set('editingTopic', false);
|
this.set('editingTopic', false);
|
||||||
|
@ -415,63 +344,51 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
this.dockedCounter = false;
|
this.dockedCounter = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
detectDockPosition: function() {
|
updateDock: function(postView) {
|
||||||
var current, goingUp, i, increment, offset, post, postView, rows, winHeight, winOffset;
|
|
||||||
rows = $(".topic-post");
|
|
||||||
if (rows.length === 0) return;
|
|
||||||
i = parseInt(rows.length / 2, 10);
|
|
||||||
increment = parseInt(rows.length / 4, 10);
|
|
||||||
goingUp = undefined;
|
|
||||||
winOffset = window.pageYOffset || $('html').scrollTop();
|
|
||||||
winHeight = window.innerHeight || $(window).height();
|
|
||||||
while (true) {
|
|
||||||
if (i === 0 || (i >= rows.length - 1)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
current = $(rows[i]);
|
|
||||||
offset = current.offset();
|
|
||||||
if (offset.top - winHeight < winOffset) {
|
|
||||||
if (offset.top + current.outerHeight() - window.innerHeight > winOffset) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
i = i + increment;
|
|
||||||
if (goingUp !== undefined && increment === 1 && !goingUp) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
goingUp = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
i = i - increment;
|
|
||||||
if (goingUp !== undefined && increment === 1 && goingUp) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
goingUp = false;
|
|
||||||
}
|
|
||||||
if (increment > 1) {
|
|
||||||
increment = parseInt(increment / 2, 10);
|
|
||||||
goingUp = undefined;
|
|
||||||
}
|
|
||||||
if (increment === 0) {
|
|
||||||
increment = 1;
|
|
||||||
goingUp = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
postView = Ember.View.views[rows[i].id];
|
|
||||||
if (!postView) return;
|
if (!postView) return;
|
||||||
post = postView.get('post');
|
post = postView.get('post');
|
||||||
if (!post) return;
|
if (!post) return;
|
||||||
this.set('progressPosition', post.get('post_number'));
|
this.set('progressPosition', post.get('post_number'));
|
||||||
},
|
},
|
||||||
|
|
||||||
ensureDockIsTestedOnChange: (function() {
|
nonUrgentScrolled: Discourse.debounce(function(opts){
|
||||||
// this is subtle, firstPostLoaded will trigger ember to render the view containing #topic-title
|
var screenTrack = this.get('screenTrack');
|
||||||
// onScroll needs do know about it to be able to make a decision about the dock
|
if(opts.track && screenTrack) {
|
||||||
Em.run.next(this, this.onScroll);
|
screenTrack.scrolled();
|
||||||
}).observes('firstPostLoaded'),
|
}
|
||||||
|
this.set('controller.currentPost', opts.currentPost);
|
||||||
|
},500),
|
||||||
|
|
||||||
onScroll: function() {
|
scrolled: function(track) {
|
||||||
var $lastPost, firstLoaded, lastPostOffset, offset, title;
|
var $lastPost, firstLoaded, lastPostOffset, offset,
|
||||||
this.detectDockPosition();
|
title, info, rows, screenTrack, _this, currentPost;
|
||||||
|
|
||||||
|
_this = this;
|
||||||
|
rows = $('.topic-post');
|
||||||
|
info = Discourse.Eyeline.analyze(rows);
|
||||||
|
|
||||||
|
// top on screen
|
||||||
|
if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) {
|
||||||
|
this.prevPage($(rows[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom of screen
|
||||||
|
if(info.bottom === rows.length-1) {
|
||||||
|
currentPost = _this.postSeen($(rows[info.bottom]));
|
||||||
|
this.nextPage($(rows[info.bottom]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// update dock
|
||||||
|
this.updateDock(Ember.View.views[rows[info.bottom].id]);
|
||||||
|
|
||||||
|
// mark everything on screen read
|
||||||
|
$.each(info.onScreen,function(){
|
||||||
|
var seen = _this.postSeen($(rows[this]));
|
||||||
|
currentPost = currentPost || seen;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.nonUrgentScrolled({track: track!==false, currentPost: currentPost});
|
||||||
|
|
||||||
offset = window.pageYOffset || $('html').scrollTop();
|
offset = window.pageYOffset || $('html').scrollTop();
|
||||||
firstLoaded = this.get('firstPostLoaded');
|
firstLoaded = this.get('firstPostLoaded');
|
||||||
if (!this.docAt) {
|
if (!this.docAt) {
|
||||||
|
@ -518,12 +435,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (Em.String.i18n("topic.browse_all_categories")) + "</a>";
|
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (Em.String.i18n("topic.browse_all_categories")) + "</a>";
|
||||||
return Ember.String.i18n("topic.read_more", opts);
|
return Ember.String.i18n("topic.read_more", opts);
|
||||||
}
|
}
|
||||||
}).property(),
|
}).property()
|
||||||
|
|
||||||
// The window has been scrolled
|
|
||||||
scrolled: function(e) {
|
|
||||||
return this.examineRead();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Discourse.TopicView.reopenClass({
|
Discourse.TopicView.reopenClass({
|
||||||
|
|
Loading…
Reference in a new issue