Version bump

This commit is contained in:
Neil Lalonde 2015-08-06 15:33:06 -04:00
commit 3ba68857bd
170 changed files with 1967 additions and 1095 deletions

View File

@ -145,7 +145,7 @@ GEM
thor (~> 0.15)
libv8 (3.16.14.7)
listen (0.7.3)
logster (0.8.3)
logster (0.8.4.1.pre)
lru_redux (1.1.0)
mail (2.5.4)
mime-types (~> 1.16)
@ -206,7 +206,7 @@ GEM
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
onebox (1.5.22)
onebox (1.5.23)
moneta (~> 0.8)
multi_json (~> 1.11)
mustache

View File

@ -1,14 +1,12 @@
import round from "discourse/lib/round";
const Report = Discourse.Model.extend({
reportUrl: function() {
return("/admin/reports/" + this.get('type'));
}.property('type'),
reportUrl: Discourse.computed.fmt("type", "/admin/reports/%@"),
valueAt(numDaysAgo) {
if (this.data) {
var wantedDate = moment().subtract(numDaysAgo, 'days').format('YYYY-MM-DD');
var item = this.data.find( function(d) { return d.x === wantedDate; } );
const wantedDate = moment().subtract(numDaysAgo, "days").format("YYYY-MM-DD");
const item = this.data.find(d => d.x === wantedDate);
if (item) {
return item.y;
}
@ -16,128 +14,117 @@ const Report = Discourse.Model.extend({
return 0;
},
sumDays(startDaysAgo, endDaysAgo) {
valueFor(startDaysAgo, endDaysAgo) {
if (this.data) {
var earliestDate = moment().subtract(endDaysAgo, 'days').startOf('day');
var latestDate = moment().subtract(startDaysAgo, 'days').startOf('day');
var d, sum = 0;
_.each(this.data,function(datum){
const earliestDate = moment().subtract(endDaysAgo, "days").startOf("day");
const latestDate = moment().subtract(startDaysAgo, "days").startOf("day");
var d, sum = 0, count = 0;
_.each(this.data, datum => {
d = moment(datum.x);
if(d >= earliestDate && d <= latestDate) {
if (d >= earliestDate && d <= latestDate) {
sum += datum.y;
count++;
}
});
if (this.get("method") === "average") { sum /= count; }
return round(sum, -2);
}
},
todayCount: function() {
return this.valueAt(0);
}.property('data'),
todayCount: function() { return this.valueAt(0); }.property("data"),
yesterdayCount: function() { return this.valueAt(1); }.property("data"),
sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data"),
thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data"),
yesterdayCount: function() {
return this.valueAt(1);
}.property('data'),
lastSevenDaysCount: function() {
return this.sumDays(1,7);
}.property('data'),
lastThirtyDaysCount: function() {
return this.sumDays(1,30);
}.property('data'),
sevenDaysAgoCount: function() {
return this.valueAt(7);
}.property('data'),
thirtyDaysAgoCount: function() {
return this.valueAt(30);
}.property('data'),
lastSevenDaysCount: function() { return this.valueFor(1, 7); }.property("data"),
lastThirtyDaysCount: function() { return this.valueFor(1, 30); }.property("data"),
yesterdayTrend: function() {
var yesterdayVal = this.valueAt(1);
var twoDaysAgoVal = this.valueAt(2);
if ( yesterdayVal > twoDaysAgoVal ) {
return 'trending-up';
} else if ( yesterdayVal < twoDaysAgoVal ) {
return 'trending-down';
const yesterdayVal = this.valueAt(1);
const twoDaysAgoVal = this.valueAt(2);
if (yesterdayVal > twoDaysAgoVal) {
return "trending-up";
} else if (yesterdayVal < twoDaysAgoVal) {
return "trending-down";
} else {
return 'no-change';
return "no-change";
}
}.property('data'),
}.property("data"),
sevenDayTrend: function() {
var currentPeriod = this.sumDays(1,7);
var prevPeriod = this.sumDays(8,14);
if ( currentPeriod > prevPeriod ) {
return 'trending-up';
} else if ( currentPeriod < prevPeriod ) {
return 'trending-down';
const currentPeriod = this.valueFor(1, 7);
const prevPeriod = this.valueFor(8, 14);
if (currentPeriod > prevPeriod) {
return "trending-up";
} else if (currentPeriod < prevPeriod) {
return "trending-down";
} else {
return 'no-change';
return "no-change";
}
}.property('data'),
}.property("data"),
thirtyDayTrend: function() {
if( this.get('prev30Days') ) {
var currentPeriod = this.sumDays(1,30);
if( currentPeriod > this.get('prev30Days') ) {
return 'trending-up';
} else if ( currentPeriod < this.get('prev30Days') ) {
return 'trending-down';
if (this.get("prev30Days")) {
const currentPeriod = this.valueFor(1, 30);
if (currentPeriod > this.get("prev30Days")) {
return "trending-up";
} else if (currentPeriod < this.get("prev30Days")) {
return "trending-down";
}
}
return 'no-change';
}.property('data', 'prev30Days'),
return "no-change";
}.property("data", "prev30Days"),
icon: function() {
switch( this.get('type') ) {
case 'flags':
return 'flag';
case 'likes':
return 'heart';
default:
return null;
switch (this.get("type")) {
case "flags": return "flag";
case "likes": return "heart";
default: return null;
}
}.property('type'),
}.property("type"),
method: function() {
if (this.get("type") === "time_to_first_response") {
return "average";
} else {
return "sum";
}
}.property("type"),
percentChangeString(val1, val2) {
var val = ((val1 - val2) / val2) * 100;
if( isNaN(val) || !isFinite(val) ) {
const val = ((val1 - val2) / val2) * 100;
if (isNaN(val) || !isFinite(val)) {
return null;
} else if( val > 0 ) {
return '+' + val.toFixed(0) + '%';
} else if (val > 0) {
return "+" + val.toFixed(0) + "%";
} else {
return val.toFixed(0) + '%';
return val.toFixed(0) + "%";
}
},
changeTitle(val1, val2, prevPeriodString) {
var title = '';
var percentChange = this.percentChangeString(val1, val2);
if( percentChange ) {
title += percentChange + ' change. ';
}
title += 'Was ' + val2 + ' ' + prevPeriodString + '.';
const percentChange = this.percentChangeString(val1, val2);
var title = "";
if (percentChange) { title += percentChange + " change. "; }
title += "Was " + val2 + " " + prevPeriodString + ".";
return title;
},
yesterdayCountTitle: function() {
return this.changeTitle( this.valueAt(1), this.valueAt(2),'two days ago');
}.property('data'),
return this.changeTitle(this.valueAt(1), this.valueAt(2), "two days ago");
}.property("data"),
sevenDayCountTitle: function() {
return this.changeTitle( this.sumDays(1,7), this.sumDays(8,14), 'two weeks ago');
}.property('data'),
return this.changeTitle(this.valueFor(1, 7), this.valueFor(8, 14), "two weeks ago");
}.property("data"),
thirtyDayCountTitle: function() {
return this.changeTitle( this.sumDays(1,30), this.get('prev30Days'), 'in the previous 30 day period');
}.property('data'),
return this.changeTitle(this.valueFor(1, 30), this.get("prev30Days"), "in the previous 30 day period");
}.property("data"),
dataReversed: function() {
return this.get('data').toArray().reverse();
}.property('data')
return this.get("data").toArray().reverse();
}.property("data")
});

View File

@ -1,5 +1,4 @@
<div class="container">
{{global-notice}}
<div class="row">
<div class="full-width">

View File

@ -32,7 +32,8 @@ export default Ember.Component.extend({
if (this.get('content')) {
const self = this;
this.get('content').forEach(function(o) {
let val = o[self.get('valueAttribute')] || o;
let val = o[self.get('valueAttribute')];
if (typeof val === "undefined") { val = o; }
if (!Em.isNone(val)) { val = val.toString(); }
const selectedText = (val === selected) ? "selected" : "";

View File

@ -0,0 +1,65 @@
export default Ember.Component.extend({
classNames: ['controls'],
notificationsPermission: function() {
if (this.get('isNotSupported')) return '';
return Notification.permission;
}.property(),
notificationsDisabled: function(_, value) {
if (arguments.length > 1) {
localStorage.setItem('notifications-disabled', value);
}
return localStorage.getItem('notifications-disabled');
}.property(),
isNotSupported: function() {
return !window['Notification'];
}.property(),
isDefaultPermission: function() {
if (this.get('isNotSupported')) return false;
return Notification.permission === "default";
}.property('isNotSupported', 'notificationsPermission'),
isDeniedPermission: function() {
if (this.get('isNotSupported')) return false;
return Notification.permission === "denied";
}.property('isNotSupported', 'notificationsPermission'),
isGrantedPermission: function() {
if (this.get('isNotSupported')) return false;
return Notification.permission === "granted";
}.property('isNotSupported', 'notificationsPermission'),
isEnabled: function() {
if (!this.get('isGrantedPermission')) return false;
return !this.get('notificationsDisabled');
}.property('isGrantedPermission', 'notificationsDisabled'),
actions: {
requestPermission() {
const self = this;
Notification.requestPermission(function() {
self.propertyDidChange('notificationsPermission');
});
},
recheckPermission() {
this.propertyDidChange('notificationsPermission');
},
turnoff() {
this.set('notificationsDisabled', 'disabled');
this.propertyDidChange('notificationsPermission');
},
turnon() {
this.set('notificationsDisabled', '');
this.propertyDidChange('notificationsPermission');
}
}
});

View File

@ -1,3 +1,5 @@
/* You might be looking for navigation-item. */
export default Ember.Component.extend({
tagName: 'li',
classNameBindings: ['active'],

View File

@ -10,30 +10,26 @@ const icons = {
'pinned_globally.enabled': 'thumb-tack',
'pinned_globally.disabled': 'thumb-tack unpinned',
'visible.enabled': 'eye',
'visible.disabled': 'eye-slash'
'visible.disabled': 'eye-slash',
'split_topic': 'sign-out'
};
export function actionDescription(actionCode, createdAt) {
return function() {
const ac = this.get(actionCode);
if (ac) {
const dt = new Date(this.get(createdAt));
const when = Discourse.Formatter.relativeAge(dt, {format: 'medium-with-ago'});
return I18n.t(`action_codes.${ac}`, {when}).htmlSafe();
}
}.property(actionCode, createdAt);
}
export default Ember.Component.extend({
layoutName: 'components/small-action', // needed because `time-gap` inherits from this
classNames: ['small-action'],
description: function() {
const actionCode = this.get('actionCode');
if (actionCode) {
const dt = new Date(this.get('post.created_at'));
const when = Discourse.Formatter.relativeAge(dt, {format: 'medium-with-ago'});
var result = I18n.t(`action_codes.${actionCode}`, {when});
var cooked = this.get('post.cooked');
result = "<p>" + result + "</p>";
if (!Em.isEmpty(cooked)) {
result += "<div class='custom-message'>" + cooked + "</div>";
}
return result;
}
}.property('actionCode', 'post.created_at', 'post.cooked'),
description: actionDescription('actionCode', 'post.created_at'),
icon: function() {
return icons[this.get('actionCode')] || 'exclamation';

View File

@ -0,0 +1,13 @@
import { actionDescription } from "discourse/components/small-action";
export default Ember.Component.extend({
classNameBindings: [":item", "item.hidden", "item.deleted", "moderatorAction"],
moderatorAction: Discourse.computed.propertyEqual("item.post_type", "site.post_types.moderator_action"),
actionDescription: actionDescription("item.action_code", "item.created_at"),
actions: {
removeBookmark(userAction) {
this.sendAction("removeBookmark", userAction);
}
}
});

View File

@ -413,7 +413,7 @@ export default Ember.ObjectController.extend(Presence, {
}
// we need a draft sequence for the composer to work
if (opts.draftSequence === void 0) {
if (opts.draftSequence === undefined) {
return Discourse.Draft.get(opts.draftKey).then(function(data) {
opts.draftSequence = data.draft_sequence;
opts.draft = data.draft;

View File

@ -1,32 +1,39 @@
import DiscourseController from 'discourse/controllers/controller';
import { translateResults } from 'discourse/lib/search-for-term';
import DiscourseController from "discourse/controllers/controller";
import { translateResults } from "discourse/lib/search-for-term";
export default DiscourseController.extend({
loading: Em.computed.not('model'),
queryParams: ['q'],
needs: ["application"],
loading: Em.computed.not("model"),
queryParams: ["q"],
q: null,
modelChanged: function(){
if (this.get('searchTerm') !== this.get('q')) {
this.set('searchTerm', this.get('q'));
}
}.observes('model'),
qChanged: function(){
var model = this.get('model');
if (model && this.get('model.q') !== this.get('q')){
this.set('searchTerm', this.get('q'));
this.send('search');
modelChanged: function() {
if (this.get("searchTerm") !== this.get("q")) {
this.set("searchTerm", this.get("q"));
}
}.observes('q'),
}.observes("model"),
qChanged: function() {
const model = this.get("model");
if (model && this.get("model.q") !== this.get("q")) {
this.set("searchTerm", this.get("q"));
this.send("search");
}
}.observes("q"),
_showFooter: function() {
this.set("controllers.application.showFooter", !this.get("loading"));
}.observes("loading"),
actions: {
search: function(){
var self = this;
this.set('q', this.get('searchTerm'));
this.set('model', null);
search() {
this.set("q", this.get("searchTerm"));
this.set("model", null);
Discourse.ajax('/search', {data: {q: this.get('searchTerm')}}).then(function(results) {
self.set('model', translateResults(results) || {});
self.set('model.q', self.get('q'));
Discourse.ajax("/search", { data: { q: this.get("searchTerm") } }).then(results => {
this.set("model", translateResults(results) || {});
this.set("model.q", this.get("q"));
});
}
}

View File

@ -1,12 +1,15 @@
import Presence from 'discourse/mixins/presence';
import SelectedPostsCount from 'discourse/mixins/selected-posts-count';
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
import { movePosts, mergeTopic } from 'discourse/models/topic';
// Modal related to merging of topics
export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, Presence, {
export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, Presence, {
needs: ['topic'],
saving: false,
selectedTopicId: null,
topicController: Em.computed.alias('controllers.topic'),
selectedPosts: Em.computed.alias('topicController.selectedPosts'),
selectedReplies: Em.computed.alias('topicController.selectedReplies'),
@ -22,38 +25,40 @@ export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, P
return I18n.t('topic.merge_topic.title');
}.property('saving'),
onShow: function() {
onShow() {
this.set('controllers.modal.modalClass', 'split-modal');
},
actions: {
movePostsToExistingTopic: function() {
movePostsToExistingTopic() {
const topicId = this.get('model.id');
this.set('saving', true);
var promise = null;
let promise = null;
if (this.get('allPostsSelected')) {
promise = Discourse.Topic.mergeTopic(this.get('id'), this.get('selectedTopicId'));
promise = mergeTopic(topicId, this.get('selectedTopicId'));
} else {
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }),
replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); });
const postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); });
const replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); });
promise = Discourse.Topic.movePosts(this.get('id'), {
promise = movePosts(topicId, {
destination_topic_id: this.get('selectedTopicId'),
post_ids: postIds,
reply_post_ids: replyPostIds
});
}
var mergeTopicController = this;
const self = this;
promise.then(function(result) {
// Posts moved
mergeTopicController.send('closeModal');
mergeTopicController.get('topicController').send('toggleMultiSelect');
self.send('closeModal');
self.get('topicController').send('toggleMultiSelect');
Em.run.next(function() { Discourse.URL.routeTo(result.url); });
}, function() {
// Error moving posts
mergeTopicController.flash(I18n.t('topic.merge_topic.error'));
mergeTopicController.set('saving', false);
}).catch(function() {
self.flash(I18n.t('topic.merge_topic.error'));
}).finally(function() {
self.set('saving', false);
});
return false;
}

View File

@ -1,15 +1,20 @@
import Presence from 'discourse/mixins/presence';
import SelectedPostsCount from 'discourse/mixins/selected-posts-count';
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
import { extractError } from 'discourse/lib/ajax-error';
import { movePosts } from 'discourse/models/topic';
// Modal related to auto closing of topics
export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, Presence, {
export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, Presence, {
needs: ['topic'],
topicName: null,
saving: false,
categoryId: null,
topicController: Em.computed.alias('controllers.topic'),
selectedPosts: Em.computed.alias('topicController.selectedPosts'),
selectedReplies: Em.computed.alias('topicController.selectedReplies'),
allPostsSelected: Em.computed.alias('topicController.allPostsSelected'),
buttonDisabled: function() {
if (this.get('saving')) return true;
@ -21,7 +26,7 @@ export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, P
return I18n.t('topic.split_topic.action');
}.property('saving'),
onShow: function() {
onShow() {
this.setProperties({
'controllers.modal.modalClass': 'split-modal',
saving: false,
@ -31,39 +36,29 @@ export default ObjectController.extend(SelectedPostsCount, ModalFunctionality, P
},
actions: {
movePostsToNewTopic: function() {
movePostsToNewTopic() {
this.set('saving', true);
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }),
replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); }),
self = this,
categoryId = this.get('categoryId'),
saveOpts = {
title: this.get('topicName'),
post_ids: postIds,
reply_post_ids: replyPostIds
};
const postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }),
replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); }),
self = this,
categoryId = this.get('categoryId'),
saveOpts = {
title: this.get('topicName'),
post_ids: postIds,
reply_post_ids: replyPostIds
};
if (!Ember.isNone(categoryId)) { saveOpts.category_id = categoryId; }
Discourse.Topic.movePosts(this.get('id'), saveOpts).then(function(result) {
movePosts(this.get('model.id'), saveOpts).then(function(result) {
// Posts moved
self.send('closeModal');
self.get('topicController').send('toggleMultiSelect');
Em.run.next(function() { Discourse.URL.routeTo(result.url); });
Ember.run.next(function() { Discourse.URL.routeTo(result.url); });
}).catch(function(xhr) {
var error = I18n.t('topic.split_topic.error');
if (xhr) {
var json = xhr.responseJSON;
if (json && json.errors) {
error = json.errors[0];
}
}
// Error moving posts
self.flash(error);
self.flash(extractError(xhr, I18n.t('topic.split_topic.error')));
}).finally(function() {
self.set('saving', false);
});
return false;

View File

@ -1,8 +1,8 @@
export default Ember.Controller.extend({
showLoginButton: Em.computed.equal('model.path', 'login'),
showLoginButton: Em.computed.equal("model.path", "login"),
actions: {
markFaqRead: function() {
markFaqRead() {
if (this.currentUser) {
Discourse.ajax("/users/read-faq", { method: "POST" });
}

View File

@ -725,7 +725,8 @@ export default ObjectController.extend(SelectedPostsCount, BufferedContent, {
},
_showFooter: function() {
this.set("controllers.application.showFooter", this.get("model.postStream.loadedAllPosts"));
}.observes("model.postStream.loadedAllPosts")
const showFooter = this.get("model.postStream.loaded") && this.get("model.postStream.loadedAllPosts");
this.set("controllers.application.showFooter", showFooter);
}.observes("model.postStream.{loaded,loadedAllPosts}")
});

View File

@ -5,7 +5,7 @@ export default Ember.ObjectController.extend({
_showFooter: function() {
var showFooter;
if (this.get("userActionType")) {
var stat = _.find(this.get("model.stats"), { action_type: this.get("userActionType") });
const stat = _.find(this.get("model.stats"), { action_type: this.get("userActionType") });
showFooter = stat && stat.count <= this.get("model.stream.itemsLoaded");
} else {
showFooter = this.get("model.statsCountNonPM") <= this.get("model.stream.itemsLoaded");

View File

@ -1,19 +1,24 @@
export default Ember.Controller.extend({
queryParams: ['period', 'order', 'asc', 'name'],
period: 'weekly',
order: 'likes_received',
needs: ["application"],
queryParams: ["period", "order", "asc", "name"],
period: "weekly",
order: "likes_received",
asc: null,
name: '',
name: "",
showTimeRead: Ember.computed.equal('period', 'all'),
showTimeRead: Ember.computed.equal("period", "all"),
_setName: Discourse.debounce(function() {
this.set('name', this.get('nameInput'));
}, 500).observes('nameInput'),
this.set("name", this.get("nameInput"));
}, 500).observes("nameInput"),
_showFooter: function() {
this.set("controllers.application.showFooter", !this.get("model.canLoadMore"));
}.observes("model.canLoadMore"),
actions: {
loadMore() {
this.get('model').loadMore();
this.get("model").loadMore();
}
}
});

View File

@ -188,15 +188,8 @@ function hoistCodeBlocksAndSpans(text) {
// /!\ the order is important /!\
// <pre>...</pre> code blocks
text = text.replace(/(^\n*|\n\n)<pre>([\s\S]*?)<\/pre>/ig, function(_, before, content) {
var hash = md5(content);
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
return before + "<pre>" + hash + "</pre>";
});
// fenced code blocks (AKA GitHub code blocks)
text = text.replace(/(^\n*|\n\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) {
text = text.replace(/(^\n*|\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) {
var hash = md5(content);
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
return before + "```" + language + "\n" + hash + "\n```";
@ -218,6 +211,13 @@ function hoistCodeBlocksAndSpans(text) {
return before + " " + hash + "\n";
});
// <pre>...</pre> code blocks
text = text.replace(/(\s|^)<pre>([\s\S]*?)<\/pre>/ig, function(_, before, content) {
var hash = md5(content);
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
return before + "<pre>" + hash + "</pre>";
});
// code spans (double & single `)
["``", "`"].forEach(function(delimiter) {
var regexp = new RegExp("(^|[^`])" + delimiter + "([^`\\n]+?)" + delimiter + "([^`]|$)", "g");
@ -277,11 +277,19 @@ Discourse.Dialect = {
// If we hoisted out anything, put it back
var keys = Object.keys(hoisted);
if (keys.length) {
keys.forEach(function(key) {
var found = true;
var unhoist = function(key) {
result = result.replace(new RegExp(key, "g"), function() {
found = true;
return hoisted[key];
});
});
};
while(found) {
found = false;
keys.forEach(unhoist);
}
}
return result.trim();

View File

@ -26,7 +26,9 @@ Discourse.BBCode.register('quote', {noWrap: true, singlePara: true}, function(co
if (options.lookupAvatarByPostNumber) {
// client-side, we can retrieve the avatar from the post
var postNumber = parseInt(params['data-post'], 10);
avatarImg = options.lookupAvatarByPostNumber(postNumber);
var topicId = parseInt(params['data-topic'], 10);
avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId);
} else if (options.lookupAvatar) {
// server-side, we need to lookup the avatar from the username
avatarImg = options.lookupAvatar(username);

View File

@ -47,7 +47,7 @@
**/
let _connectorCache;
let _connectorCache, _rawCache;
function findOutlets(collection, callback) {
@ -73,6 +73,7 @@ function findOutlets(collection, callback) {
function buildConnectorCache() {
_connectorCache = {};
_rawCache = {};
const uniqueViews = {};
findOutlets(requirejs._eak_seen, function(outletName, resource, uniqueName) {
@ -93,10 +94,23 @@ function buildConnectorCache() {
// We are going to add it back with the proper template
_connectorCache[outletName].removeObject(viewClass);
} else {
viewClass = Em.View.extend({ classNames: [outletName + '-outlet', uniqueName] });
if (!/\.raw$/.test(uniqueName)) {
viewClass = Em.View.extend({ classNames: [outletName + '-outlet', uniqueName] });
}
}
if (viewClass) {
_connectorCache[outletName].pushObject(viewClass.extend(mixin));
} else {
// we have a raw template
if (!_rawCache[outletName]) {
_rawCache[outletName] = [];
}
_rawCache[outletName].push(Ember.TEMPLATES[resource]);
}
_connectorCache[outletName].pushObject(viewClass.extend(mixin));
});
}
var _viewInjections;
@ -113,6 +127,24 @@ function viewInjections(container) {
return _viewInjections;
}
// unbound version of outlets, only has a template
Handlebars.registerHelper('plugin-outlet', function(name){
if (!_rawCache) { buildConnectorCache(); }
const functions = _rawCache[name];
if (functions) {
var output = [];
for(var i=0; i<functions.length; i++){
output.push(functions[i]({context: this}));
}
return new Handlebars.SafeString(output.join(""));
}
});
Ember.HTMLBars._registerHelper('plugin-outlet', function(params, hash, options, env) {
const connectionName = params[0];
@ -139,3 +171,5 @@ Ember.HTMLBars._registerHelper('plugin-outlet', function(params, hash, options,
}
}
});

View File

@ -5,5 +5,7 @@ registerUnbound('topic-link', function(topic) {
var url = topic.linked_post_number ? topic.urlForPostNumber(topic.linked_post_number) : topic.get('lastUnreadUrl');
var extraClass = topic.get('last_read_post_number') === topic.get('highest_post_number') ? " visited" : "";
return new Handlebars.SafeString("<a href='" + url + "' class='title" + extraClass + "'>" + title + "</a>");
var string = "<a href='" + url + "' class='title" + extraClass + "'>" + title + "</a>";
return new Handlebars.SafeString(string);
});

View File

@ -0,0 +1,15 @@
export default {
name: "show-footer",
initialize(container) {
const router = container.lookup("router:main");
const application = container.lookup("controller:application");
// only take care of hiding the footer here
// controllers MUST take care of displaying it
router.on("willTransition", () => {
application.set("showFooter", false);
return true;
});
}
};

View File

@ -35,6 +35,9 @@ export default {
});
bus.subscribe('/queue_counts', (data) => {
user.set('post_queue_new_count', data.post_queue_new_count);
if (data.post_queue_new_count > 0) {
user.set('show_queued_posts', 1);
}
});
}

View File

@ -1,4 +1,4 @@
function extractError(error) {
export function extractError(error, defaultMessage) {
if (error instanceof Error) {
Ember.Logger.error(error.stack);
}
@ -42,7 +42,7 @@ function extractError(error) {
}
}
return parsedError || I18n.t('generic_error');
return parsedError || defaultMessage || I18n.t('generic_error');
}
export function throwAjaxError(undoCallback) {

View File

@ -94,6 +94,7 @@ function onNotification(data) {
if (!liveEnabled) { return; }
if (!primaryTab) { return; }
if (!isIdle()) { return; }
if (localStorage.getItem('notifications-disabled')) { return; }
const notificationTitle = I18n.t(i18nKey(data.notification_type), {
site_title: Discourse.SiteSettings.title,

View File

@ -11,7 +11,7 @@ var groups = [
{
name: "nature",
fullname: "Nature",
tabicon: "leaves",
tabicon: "evergreen_tree",
icons: ["seedling", "evergreen_tree", "deciduous_tree", "palm_tree", "cactus", "tulip", "cherry_blossom", "rose", "hibiscus", "sunflower", "blossom", "bouquet", "ear_of_rice", "herb", "four_leaf_clover", "maple_leaf", "fallen_leaf", "leaves", "mushroom", "chestnut", "rat", "mouse2", "mouse", "hamster", "ox", "water_buffalo", "cow2", "cow", "tiger2", "leopard", "tiger", "rabbit2", "rabbit", "cat2", "cat", "racehorse", "horse", "ram", "sheep", "goat", "rooster", "chicken", "baby_chick", "hatching_chick", "hatched_chick", "bird", "penguin", "elephant", "dromedary_camel", "camel", "boar", "pig2", "pig", "pig_nose", "dog2", "poodle", "dog", "wolf", "bear", "koala", "panda_face", "monkey_face", "see_no_evil", "hear_no_evil", "speak_no_evil", "monkey", "dragon", "dragon_face", "crocodile", "snake", "turtle", "frog", "whale2", "whale", "dolphin", "octopus", "fish", "tropical_fish", "blowfish", "shell", "snail", "bug", "ant", "bee", "beetle", "feet", "zap", "fire", "crescent_moon", "sunny", "partly_sunny", "cloud", "droplet", "sweat_drops", "umbrella", "dash", "snowflake", "star2", "star", "stars", "sunrise_over_mountains", "sunrise", "rainbow", "ocean", "volcano", "milky_way", "mount_fuji", "japan", "globe_with_meridians", "earth_africa", "earth_americas", "earth_asia", "new_moon", "waxing_crescent_moon", "first_quarter_moon", "moon", "full_moon", "waning_gibbous_moon", "last_quarter_moon", "waning_crescent_moon", "new_moon_with_face", "full_moon_with_face", "first_quarter_moon_with_face", "last_quarter_moon_with_face", "sun_with_face"]
},
{
@ -144,7 +144,7 @@ var toolbar = function(selected){
var icon = g.tabicon;
var title = g.fullname;
if (g.name === "recent") {
icon = "star2";
icon = "star";
title = "Recent";
} else if (g.name === "ungrouped") {
icon = g.icons[0];

View File

@ -1,39 +1,42 @@
import ShowFooter from "discourse/mixins/show-footer";
var configs = {
'faq': 'faq_url',
'tos': 'tos_url',
'privacy': 'privacy_policy_url'
const configs = {
"faq": "faq_url",
"tos": "tos_url",
"privacy": "privacy_policy_url"
};
export default function(page) {
return Discourse.Route.extend(ShowFooter, {
renderTemplate: function() {
this.render('static');
export default (page) => {
return Discourse.Route.extend({
renderTemplate() {
this.render("static");
},
beforeModel: function(transition) {
var configKey = configs[page];
beforeModel(transition) {
const configKey = configs[page];
if (configKey && Discourse.SiteSettings[configKey].length > 0) {
transition.abort();
Discourse.URL.redirectTo(Discourse.SiteSettings[configKey]);
}
},
activate: function() {
activate() {
this._super();
// Scroll to an element if exists
Discourse.URL.scrollToId(document.location.hash);
},
model: function() {
model() {
return Discourse.StaticPage.find(page);
},
setupController: function(controller, model) {
this.controllerFor('static').set('model', model);
setupController(controller, model) {
this.controllerFor("static").set("model", model);
},
actions: {
didTransition() {
this.controllerFor("application").set("showFooter", true);
return true;
}
}
});
}
};

View File

@ -47,7 +47,8 @@ Discourse.Ajax = Em.Mixin.create({
if (_trackView && (!args.type || args.type === "GET")) {
_trackView = false;
args.headers['Discourse-Track-View'] = true;
// DON'T CHANGE: rack is prepending "HTTP_" in the header's name
args.headers['DISCOURSE_TRACK_VIEW'] = true;
}
args.success = function(data, textStatus, xhr) {

View File

@ -33,6 +33,7 @@ Discourse.Scrolling = Em.Mixin.create({
}
Discourse.ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name);
Em.run.scheduleOnce('afterRender', onScrollMethod);
},
/**

View File

@ -1,15 +0,0 @@
export default Em.Mixin.create({
actions: {
didTransition() {
Em.run.schedule("afterRender", () => {
this.controllerFor("application").set("showFooter", true);
});
return true;
},
willTransition() {
this.controllerFor("application").set("showFooter", false);
return true;
}
}
});

View File

@ -22,7 +22,9 @@ const CLOSED = 'closed',
topic_id: 'topic.id',
is_warning: 'isWarning',
archetype: 'archetypeId',
target_usernames: 'targetUsernames'
target_usernames: 'targetUsernames',
typing_duration_msecs: 'typingTime',
composer_open_duration_msecs: 'composerTime'
},
_edit_topic_serializer = {
@ -52,6 +54,31 @@ const Composer = RestModel.extend({
viewOpen: Em.computed.equal('composeState', OPEN),
viewDraft: Em.computed.equal('composeState', DRAFT),
composeStateChanged: function() {
var oldOpen = this.get('composerOpened');
if (this.get('composeState') === OPEN) {
this.set('composerOpened', oldOpen || new Date());
} else {
if (oldOpen) {
var oldTotal = this.get('composerTotalOpened') || 0;
this.set('composerTotalOpened', oldTotal + (new Date() - oldOpen));
}
this.set('composerOpened', null);
}
}.observes('composeState'),
composerTime: function() {
var total = this.get('composerTotalOpened') || 0;
var oldOpen = this.get('composerOpened');
if (oldOpen) {
total += (new Date() - oldOpen);
}
return total;
}.property().volatile(),
archetype: function() {
return this.get('archetypes').findProperty('id', this.get('archetypeId'));
}.property('archetypeId'),
@ -60,6 +87,12 @@ const Composer = RestModel.extend({
return this.set('metaData', Em.Object.create());
}.observes('archetype'),
// view detected user is typing
typing: _.throttle(function(){
var typingTime = this.get("typingTime") || 0;
this.set("typingTime", typingTime + 100);
}, 100, {leading: false, trailing: true}),
editingFirstPost: Em.computed.and('editingPost', 'post.firstPost'),
canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'),
canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'),
@ -349,7 +382,9 @@ const Composer = RestModel.extend({
composeState: opts.composerState || OPEN,
action: opts.action,
topic: opts.topic,
targetUsernames: opts.usernames
targetUsernames: opts.usernames,
composerTotalOpened: opts.composerTime,
typingTime: opts.typingTime
});
if (opts.post) {
@ -420,7 +455,10 @@ const Composer = RestModel.extend({
post: null,
title: null,
editReason: null,
stagedPost: false
stagedPost: false,
typingTime: 0,
composerOpened: null,
composerTotalOpened: 0
});
},
@ -502,7 +540,9 @@ const Composer = RestModel.extend({
admin: user.get('admin'),
yours: true,
read: true,
wiki: false
wiki: false,
typingTime: this.get('typingTime'),
composerTime: this.get('composerTime')
});
this.serialize(_create_serializer, createdPost);
@ -603,13 +643,20 @@ const Composer = RestModel.extend({
postId: this.get('post.id'),
archetypeId: this.get('archetypeId'),
metaData: this.get('metaData'),
usernames: this.get('targetUsernames')
usernames: this.get('targetUsernames'),
composerTime: this.get('composerTime'),
typingTime: this.get('typingTime')
};
this.set('draftStatus', I18n.t('composer.saving_draft_tip'));
const composer = this;
if (this._clearingStatus) {
Em.run.cancel(this._clearingStatus);
this._clearingStatus = null;
}
// try to save the draft
return Discourse.Draft.save(this.get('draftKey'), this.get('draftSequence'), data)
.then(function() {
@ -617,7 +664,20 @@ const Composer = RestModel.extend({
}).catch(function() {
composer.set('draftStatus', I18n.t('composer.drafts_offline'));
});
}
},
dataChanged: function(){
const draftStatus = this.get('draftStatus');
const self = this;
if (draftStatus && !this._clearingStatus) {
this._clearingStatus = Em.run.later(this, function(){
self.set('draftStatus', null);
self._clearingStatus = null;
}, 1000);
}
}.observes('title','reply')
});
@ -657,7 +717,9 @@ Composer.reopenClass({
metaData: draft.metaData,
usernames: draft.usernames,
draft: true,
composerState: DRAFT
composerState: DRAFT,
composerTime: draft.composerTime,
typingTime: draft.typingTime
});
}
},

View File

@ -7,7 +7,7 @@
@module Discourse
**/
Discourse.NavItem = Discourse.Model.extend({
const NavItem = Discourse.Model.extend({
displayName: function() {
var categoryName = this.get('categoryName'),
@ -25,7 +25,7 @@ Discourse.NavItem = Discourse.Model.extend({
extra.categoryName = Discourse.Formatter.toTitleCase(categoryName);
}
return I18n.t("filters." + name.replace("/", ".") + ".title", extra);
}.property('categoryName,name,count'),
}.property('categoryName', 'name', 'count'),
topicTrackingState: function() {
return Discourse.TopicTrackingState.current();
@ -45,8 +45,13 @@ Discourse.NavItem = Discourse.Model.extend({
return null;
}.property('name'),
// href from this item
href: function() {
var customHref = null;
_.each(NavItem.customNavItemHrefs, function(cb) {
customHref = cb.call(this, this);
if (customHref) { return false; }
}, this);
if (customHref) { return customHref; }
return Discourse.getURL("/") + this.get('filterMode');
}.property('filterMode'),
@ -79,10 +84,13 @@ Discourse.NavItem = Discourse.Model.extend({
});
Discourse.NavItem.reopenClass({
NavItem.reopenClass({
extraArgsCallbacks: [],
customNavItemHrefs: [],
// create a nav item from the text, will return null if there is not valid nav item for this particular text
fromText: function(text, opts) {
fromText(text, opts) {
var split = text.split(","),
name = split[0],
testName = name.split("/")[0],
@ -92,13 +100,17 @@ Discourse.NavItem.reopenClass({
if (!Discourse.Category.list() && testName === "categories") return null;
if (!Discourse.Site.currentProp('top_menu_items').contains(testName)) return null;
var args = { name: name, hasIcon: name === "unread" };
var args = { name: name, hasIcon: name === "unread" }, extra = null, self = this;
if (opts.category) { args.category = opts.category; }
if (opts.noSubcategories) { args.noSubcategories = true; }
_.each(NavItem.extraArgsCallbacks, function(cb) {
extra = cb.call(self, text, opts);
_.merge(args, extra);
});
return Discourse.NavItem.create(args);
},
buildList: function(category, args) {
buildList(category, args) {
args = args || {};
if (category) { args.category = category }
@ -118,3 +130,11 @@ Discourse.NavItem.reopenClass({
}
});
export default NavItem;
export function extraNavItemProperties(cb) {
NavItem.extraArgsCallbacks.push(cb);
}
export function customNavItemHref(cb) {
NavItem.customNavItemHrefs.push(cb);
}

View File

@ -1,7 +1,6 @@
import RestModel from 'discourse/models/rest';
import Model from 'discourse/models/model';
function topicsFrom(result, store) {
if (!result) { return; }

View File

@ -1,3 +1,4 @@
import { flushMap } from 'discourse/models/store';
import RestModel from 'discourse/models/rest';
const Topic = RestModel.extend({
@ -462,28 +463,6 @@ Topic.reopenClass({
return Discourse.ajax(url + ".json", {data: data});
},
mergeTopic(topicId, destinationTopicId) {
const 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(topicId, opts) {
const 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(topicId, opts) {
const promise = Discourse.ajax("/t/" + topicId + "/change-owner", {
type: 'POST',
@ -523,4 +502,24 @@ Topic.reopenClass({
}
});
function moveResult(result) {
if (result.success) {
// We should be hesitant to flush the map but moving ids is one rare case
flushMap();
return result;
}
throw "error moving posts topic";
}
export function movePosts(topicId, data) {
return Discourse.ajax("/t/" + topicId + "/move-posts", { type: 'POST', data }).then(moveResult);
}
export function mergeTopic(topicId, destinationTopicId) {
return Discourse.ajax("/t/" + topicId + "/merge-topic", {
type: 'POST',
data: {destination_topic_id: destinationTopicId}
}).then(moveResult);
}
export default Topic;

View File

@ -1,13 +1,16 @@
import ShowFooter from "discourse/mixins/show-footer";
export default Discourse.Route.extend(ShowFooter, {
model: function() {
return Discourse.ajax("/about.json").then(function(result) {
return result.about;
});
export default Discourse.Route.extend({
model() {
return Discourse.ajax("/about.json").then(result => result.about);
},
titleToken: function() {
titleToken() {
return I18n.t('about.simple_title');
},
actions: {
didTransition() {
this.controllerFor("application").set("showFooter", true);
return true;
}
}
});

View File

@ -53,7 +53,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
error(err, transition) {
if (err.status === 404) {
// 404
this.intermediateTransitionTo('unknown');
this.transitionTo('unknown');
return;
}
@ -74,7 +74,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
}
exceptionController.setProperties({ lastTransition: transition, thrown: err });
this.intermediateTransitionTo('exception');
this.transitionTo('exception');
},
showLogin: unlessReadOnly('handleShowLogin'),

View File

@ -1,17 +1,20 @@
import ShowFooter from "discourse/mixins/show-footer";
export default Discourse.Route.extend(ShowFooter, {
model: function() {
if (PreloadStore.get('badges')) {
return PreloadStore.getAndRemove('badges').then(function(json) {
return Discourse.Badge.createFromJson(json);
});
export default Discourse.Route.extend({
model() {
if (PreloadStore.get("badges")) {
return PreloadStore.getAndRemove("badges").then(json => Discourse.Badge.createFromJson(json));
} else {
return Discourse.Badge.findAll({onlyListable: true});
return Discourse.Badge.findAll({ onlyListable: true });
}
},
titleToken: function() {
return I18n.t('badges.title');
titleToken() {
return I18n.t("badges.title");
},
actions: {
didTransition() {
this.controllerFor("application").set("showFooter", true);
return true;
}
}
});

View File

@ -1,43 +1,41 @@
import ShowFooter from "discourse/mixins/show-footer";
export default Discourse.Route.extend(ShowFooter, {
export default Discourse.Route.extend({
actions: {
didTransition: function() {
didTransition() {
this.controllerFor("badges/show")._showFooter();
return true;
}
},
serialize: function(model) {
return {id: model.get('id'), slug: model.get('name').replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase()};
serialize(model) {
return {
id: model.get("id"),
slug: model.get("name").replace(/[^A-Za-z0-9_]+/g, "-").toLowerCase()
};
},
model: function(params) {
if (PreloadStore.get('badge')) {
return PreloadStore.getAndRemove('badge').then(function(json) {
return Discourse.Badge.createFromJson(json);
});
model(params) {
if (PreloadStore.get("badge")) {
return PreloadStore.getAndRemove("badge").then(json => Discourse.Badge.createFromJson(json));
} else {
return Discourse.Badge.findById(params.id);
}
},
afterModel: function(model) {
var self = this;
return Discourse.UserBadge.findByBadgeId(model.get('id')).then(function(userBadges) {
self.userBadges = userBadges;
afterModel(model) {
return Discourse.UserBadge.findByBadgeId(model.get("id")).then(userBadges => {
this.userBadges = userBadges;
});
},
titleToken: function() {
var model = this.modelFor('badges.show');
titleToken() {
const model = this.modelFor("badges.show");
if (model) {
return model.get('displayName');
return model.get("displayName");
}
},
setupController: function(controller, model) {
controller.set('model', model);
controller.set('userBadges', this.userBadges);
setupController(controller, model) {
controller.set("model", model);
controller.set("userBadges", this.userBadges);
}
});

View File

@ -1,31 +1,29 @@
import ShowFooter from "discourse/mixins/show-footer";
export default function (filter) {
return Discourse.Route.extend(ShowFooter, {
return Discourse.Route.extend({
actions: {
didTransition: function() {
this.controllerFor('user').set('indexStream', true);
didTransition() {
this.controllerFor("user").set("indexStream", true);
this.controllerFor("user-posts")._showFooter();
return true;
}
},
model: function () {
model() {
return this.modelFor("user").get("postsStream");
},
afterModel: function () {
afterModel() {
return this.modelFor("user").get("postsStream").filterBy(filter);
},
setupController: function(controller, model) {
setupController(controller, model) {
// initialize "canLoadMore"
model.set("canLoadMore", model.get("itemsLoaded") === 60);
this.controllerFor("user-posts").set("model", model);
},
renderTemplate: function() {
renderTemplate() {
this.render("user/posts", { into: "user" });
}
});

View File

@ -1,36 +1,35 @@
import UserTopicListRoute from "discourse/routes/user-topic-list";
import ShowFooter from "discourse/mixins/show-footer";
// A helper to build a user topic list route
export default function (viewName, path) {
return UserTopicListRoute.extend(ShowFooter, {
export default (viewName, path) => {
return UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.messages_received,
actions: {
didTransition: function() {
didTransition() {
this.controllerFor("user-topics-list")._showFooter();
return true;
}
},
model: function() {
return this.store.findFiltered('topicList', {filter: 'topics/' + path + '/' + this.modelFor('user').get('username_lower')});
model() {
return this.store.findFiltered("topicList", { filter: "topics/" + path + "/" + this.modelFor("user").get("username_lower") });
},
setupController: function() {
setupController() {
this._super.apply(this, arguments);
this.controllerFor('user-topics-list').setProperties({
this.controllerFor("user-topics-list").setProperties({
hideCategory: true,
showParticipants: true
});
this.controllerFor('user').set('pmView', viewName);
this.controllerFor('search').set('contextType', 'private_messages');
this.controllerFor("user").set("pmView", viewName);
this.controllerFor("search").set("contextType", "private_messages");
},
deactivate: function(){
this.controllerFor('search').set('contextType', 'user');
deactivate() {
this.controllerFor("search").set("contextType", "user");
}
});
}
};

View File

@ -4,7 +4,7 @@ const DiscourseRoute = Ember.Route.extend({
// changes
resfreshQueryWithoutTransition: false,
refresh: function() {
refresh() {
if (!this.refreshQueryWithoutTransition) { return this._super(); }
if (!this.router.router.activeTransition) {
@ -17,13 +17,13 @@ const DiscourseRoute = Ember.Route.extend({
}
},
_refreshTitleOnce: function() {
_refreshTitleOnce() {
this.send('_collectTitleTokens', []);
},
actions: {
_collectTitleTokens: function(tokens) {
_collectTitleTokens(tokens) {
// If there's a title token method, call it and get the token
if (this.titleToken) {
const t = this.titleToken();
@ -40,19 +40,19 @@ const DiscourseRoute = Ember.Route.extend({
return true;
},
refreshTitle: function() {
refreshTitle() {
Ember.run.once(this, this._refreshTitleOnce);
}
},
redirectIfLoginRequired: function() {
redirectIfLoginRequired() {
const app = this.controllerFor('application');
if (app.get('loginRequired')) {
this.replaceWith('login');
}
},
openTopicDraft: function(model){
openTopicDraft(model){
// If there's a draft, open the create topic composer
if (model.draft) {
const composer = this.controllerFor('composer');
@ -67,7 +67,7 @@ const DiscourseRoute = Ember.Route.extend({
}
},
isPoppedState: function(transition) {
isPoppedState(transition) {
return (!transition._discourse_intercepted) && (!!transition.intent.url);
}
});

View File

@ -1,15 +1,14 @@
import ShowFooter from 'discourse/mixins/show-footer';
import showModal from 'discourse/lib/show-modal';
import showModal from "discourse/lib/show-modal";
import OpenComposer from "discourse/mixins/open-composer";
Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, ShowFooter, {
Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
renderTemplate() {
this.render('navigation/categories', { outlet: 'navigation-bar' });
this.render('discovery/categories', { outlet: 'list-container' });
this.render("navigation/categories", { outlet: "navigation-bar" });
this.render("discovery/categories", { outlet: "list-container" });
},
beforeModel() {
this.controllerFor('navigation/categories').set('filterMode', 'categories');
this.controllerFor("navigation/categories").set("filterMode", "categories");
},
model() {
@ -17,11 +16,11 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, ShowFo
// if default page is categories
PreloadStore.remove("topic_list");
return Discourse.CategoryList.list('categories').then(function(list) {
return Discourse.CategoryList.list("categories").then(function(list) {
const tracking = Discourse.TopicTrackingState.current();
if (tracking) {
tracking.sync(list, 'categories');
tracking.trackIncoming('categories');
tracking.sync(list, "categories");
tracking.trackIncoming("categories");
}
return list;
});
@ -29,15 +28,15 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, ShowFo
titleToken() {
if (Discourse.Utilities.defaultHomepage() === "categories") { return; }
return I18n.t('filters.categories.title');
return I18n.t("filters.categories.title");
},
setupController(controller, model) {
controller.set('model', model);
controller.set("model", model);
// Only show either the Create Category or Create Topic button
this.controllerFor('navigation/categories').set('canCreateCategory', model.get('can_create_category'));
this.controllerFor('navigation/categories').set('canCreateTopic', model.get('can_create_topic') && !model.get('can_create_category'));
this.controllerFor("navigation/categories").set("canCreateCategory", model.get("can_create_category"));
this.controllerFor("navigation/categories").set("canCreateTopic", model.get("can_create_topic") && !model.get("can_create_category"));
this.openTopicDraft(model);
},
@ -45,20 +44,25 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, ShowFo
actions: {
createCategory() {
const groups = this.site.groups,
everyoneName = groups.findBy('id', 0).name;
everyoneName = groups.findBy("id", 0).name;
const model = Discourse.Category.create({
color: 'AB9364', text_color: 'FFFFFF', group_permissions: [{group_name: everyoneName, permission_type: 1}],
color: "AB9364", text_color: "FFFFFF", group_permissions: [{group_name: everyoneName, permission_type: 1}],
available_groups: groups.map(g => g.name),
allow_badges: true
});
showModal('editCategory', { model });
this.controllerFor('editCategory').set('selectedTab', 'general');
showModal("editCategory", { model });
this.controllerFor("editCategory").set("selectedTab", "general");
},
createTopic() {
this.openComposer(this.controllerFor('discovery/categories'));
this.openComposer(this.controllerFor("discovery/categories"));
},
didTransition() {
this.controllerFor("application").set("showFooter", true);
return true;
}
}
});

View File

@ -2,14 +2,15 @@
The parent route for all discovery routes.
Handles the logic for showing the loading spinners.
**/
import ShowFooter from "discourse/mixins/show-footer";
import OpenComposer from "discourse/mixins/open-composer";
import { scrollTop } from 'discourse/mixins/scroll-top';
import { scrollTop } from "discourse/mixins/scroll-top";
const DiscoveryRoute = Discourse.Route.extend(OpenComposer, ShowFooter, {
redirect: function() { return this.redirectIfLoginRequired(); },
const DiscoveryRoute = Discourse.Route.extend(OpenComposer, {
redirect() {
return this.redirectIfLoginRequired();
},
beforeModel: function(transition) {
beforeModel(transition) {
if (transition.intent.url === "/" &&
transition.targetName.indexOf("discovery.top") === -1 &&
Discourse.User.currentProp("should_be_redirected_to_top")) {
@ -19,31 +20,31 @@ const DiscoveryRoute = Discourse.Route.extend(OpenComposer, ShowFooter, {
},
actions: {
loading: function() {
this.controllerFor('discovery').set("loading", true);
loading() {
this.controllerFor("discovery").set("loading", true);
return true;
},
loadingComplete: function() {
this.controllerFor('discovery').set('loading', false);
if (!this.session.get('topicListScrollPosition')) {
loadingComplete() {
this.controllerFor("discovery").set("loading", false);
if (!this.session.get("topicListScrollPosition")) {
scrollTop();
}
},
didTransition: function() {
didTransition() {
this.controllerFor("discovery")._showFooter();
this.send('loadingComplete');
this.send("loadingComplete");
return true;
},
// clear a pinned topic
clearPin: function(topic) {
clearPin(topic) {
topic.clearPin();
},
createTopic: function() {
this.openComposer(this.controllerFor('discovery/topics'));
createTopic() {
this.openComposer(this.controllerFor("discovery/topics"));
}
}

View File

@ -1,7 +1,10 @@
import ShowFooter from "discourse/mixins/show-footer";
export default Discourse.Route.extend({
serialize() { return ""; },
export default Discourse.Route.extend(ShowFooter, {
serialize: function() {
return "";
actions: {
didTransition() {
this.controllerFor("application").set("showFooter", true);
return true;
}
}
});

View File

@ -1,18 +1,23 @@
import { translateResults } from 'discourse/lib/search-for-term';
import { translateResults } from "discourse/lib/search-for-term";
export default Discourse.Route.extend({
queryParams: {
q: {
}
},
model: function(params) {
queryParams: { q: {} },
model(params) {
return PreloadStore.getAndRemove("search", function() {
return Discourse.ajax('/search', {data: {q: params.q}});
}).then(function(results){
var model = translateResults(results) || {};
return Discourse.ajax("/search", { data: { q: params.q } });
}).then(results => {
const model = translateResults(results) || {};
model.q = params.q;
return model;
});
},
actions: {
didTransition() {
this.controllerFor("full-page-search")._showFooter();
return true;
}
}
});

View File

@ -1,18 +1,14 @@
import ShowFooter from "discourse/mixins/show-footer";
export default Discourse.Route.extend(ShowFooter, {
export default Discourse.Route.extend({
actions: {
didTransition: function() {
return true;
}
didTransition() { return true; }
},
model: function() {
return this.modelFor('group').findPosts();
model() {
return this.modelFor("group").findPosts();
},
setupController: function(controller, model) {
controller.set('model', model);
this.controllerFor('group').set('showing', 'index');
setupController(controller, model) {
controller.set("model", model);
this.controllerFor("group").set("showing", "index");
}
});

View File

@ -1,12 +1,10 @@
import ShowFooter from "discourse/mixins/show-footer";
export default Discourse.Route.extend(ShowFooter, {
export default Discourse.Route.extend({
model() {
return this.modelFor('group');
return this.modelFor("group");
},
setupController(controller, model) {
this.controllerFor('group').set('showing', 'members');
this.controllerFor("group").set("showing", "members");
controller.set("model", model);
model.findMembers();
}

View File

@ -1,8 +1,7 @@
import ShowFooter from "discourse/mixins/show-footer";
import RestrictedUserRoute from "discourse/routes/restricted-user";
import showModal from 'discourse/lib/show-modal';
export default RestrictedUserRoute.extend(ShowFooter, {
export default RestrictedUserRoute.extend({
model() {
return this.modelFor('user');
},

View File

@ -4,10 +4,9 @@ let isTransitioning = false,
const SCROLL_DELAY = 500;
import ShowFooter from "discourse/mixins/show-footer";
import showModal from 'discourse/lib/show-modal';
const TopicRoute = Discourse.Route.extend(ShowFooter, {
const TopicRoute = Discourse.Route.extend({
redirect() { return this.redirectIfLoginRequired(); },
queryParams: {

View File

@ -4,9 +4,9 @@ export default UserActivityStreamRoute.extend({
userActionType: undefined,
actions: {
didTransition: function() {
didTransition() {
this._super();
this.controllerFor('user').set('indexStream', true);
this.controllerFor("user").set("indexStream", true);
return true;
}
}

View File

@ -1,39 +1,38 @@
import ShowFooter from "discourse/mixins/show-footer";
import ViewingActionType from "discourse/mixins/viewing-action-type";
export default Discourse.Route.extend(ShowFooter, ViewingActionType, {
model: function() {
return this.modelFor('user').get('stream');
export default Discourse.Route.extend(ViewingActionType, {
model() {
return this.modelFor("user").get("stream");
},
afterModel: function() {
return this.modelFor('user').get('stream').filterBy(this.get('userActionType'));
afterModel() {
return this.modelFor("user").get("stream").filterBy(this.get("userActionType"));
},
renderTemplate: function() {
this.render('user_stream');
renderTemplate() {
this.render("user_stream");
},
setupController: function(controller, model) {
controller.set('model', model);
this.viewingActionType(this.get('userActionType'));
setupController(controller, model) {
controller.set("model", model);
this.viewingActionType(this.get("userActionType"));
},
actions: {
didTransition: function() {
didTransition() {
this.controllerFor("user-activity")._showFooter();
return true;
},
removeBookmark: function(userAction) {
var user = this.modelFor('user');
Discourse.Post.updateBookmark(userAction.get('post_id'), false)
removeBookmark(userAction) {
var user = this.modelFor("user");
Discourse.Post.updateBookmark(userAction.get("post_id"), false)
.then(function() {
// remove the user action from the stream
user.get('stream').remove(userAction);
user.get("stream").remove(userAction);
// update the counts
user.get('stats').forEach(function (stat) {
user.get("stats").forEach(function (stat) {
if (stat.get("action_type") === userAction.action_type) {
stat.decrementProperty("count");
}

View File

@ -1,20 +1,20 @@
export default Discourse.Route.extend({
model: function() {
return this.modelFor('user');
model() {
return this.modelFor("user");
},
setupController: function(controller, user) {
this.controllerFor('user-activity').set('model', user);
setupController(controller, user) {
this.controllerFor("user-activity").set("model", user);
// Bring up a draft
const composerController = this.controllerFor('composer');
controller.set('model', user);
const composerController = this.controllerFor("composer");
controller.set("model", user);
if (this.currentUser) {
Discourse.Draft.get('new_private_message').then(function(data) {
Discourse.Draft.get("new_private_message").then(function(data) {
if (data.draft) {
composerController.open({
draft: data.draft,
draftKey: 'new_private_message',
draftKey: "new_private_message",
ignoreIfChanged: true,
draftSequence: data.draft_sequence
});

View File

@ -1,17 +1,23 @@
import ShowFooter from "discourse/mixins/show-footer";
import ViewingActionType from "discourse/mixins/viewing-action-type";
export default Discourse.Route.extend(ShowFooter, ViewingActionType, {
model: function() {
return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username_lower'), {grouped: true});
export default Discourse.Route.extend(ViewingActionType, {
model() {
return Discourse.UserBadge.findByUsername(this.modelFor("user").get("username_lower"), { grouped: true });
},
setupController: function(controller, model) {
setupController(controller, model) {
this.viewingActionType(-1);
controller.set('model', model);
controller.set("model", model);
},
renderTemplate: function() {
this.render('user/badges', {into: 'user'});
renderTemplate() {
this.render("user/badges", {into: "user"});
},
actions: {
didTransition() {
this.controllerFor("application").set("showFooter", true);
return true;
}
}
});

View File

@ -1,33 +1,32 @@
import ShowFooter from 'discourse/mixins/show-footer';
import showModal from 'discourse/lib/show-modal';
import showModal from "discourse/lib/show-modal";
export default Discourse.Route.extend(ShowFooter, {
export default Discourse.Route.extend({
model: function(params) {
model(params) {
this.inviteFilter = params.filter;
return Discourse.Invite.findInvitedBy(this.modelFor('user'), params.filter);
return Discourse.Invite.findInvitedBy(this.modelFor("user"), params.filter);
},
afterModel: function(model) {
afterModel(model) {
if (!model.can_see_invite_details) {
this.replaceWith('userInvited.show', 'redeemed');
this.replaceWith("userInvited.show", "redeemed");
}
},
setupController(controller, model) {
controller.setProperties({
model: model,
user: this.controllerFor('user').get('model'),
user: this.controllerFor("user").get("model"),
filter: this.inviteFilter,
searchTerm: '',
searchTerm: "",
totalInvites: model.invites.length
});
},
actions: {
showInvite() {
showModal('invite', { model: this.currentUser });
this.controllerFor('invite').reset();
showModal("invite", { model: this.currentUser });
this.controllerFor("invite").reset();
},
uploadSuccess(filename) {

View File

@ -1,7 +1,6 @@
import ShowFooter from "discourse/mixins/show-footer";
import ViewingActionType from "discourse/mixins/viewing-action-type";
export default Discourse.Route.extend(ShowFooter, ViewingActionType, {
export default Discourse.Route.extend(ViewingActionType, {
actions: {
didTransition() {
this.controllerFor("user-notifications")._showFooter();
@ -10,13 +9,12 @@ export default Discourse.Route.extend(ShowFooter, ViewingActionType, {
},
model() {
var user = this.modelFor('user');
return this.store.find('notification', {username: user.get('username')});
return this.store.find("notification", { username: this.modelFor("user").get("username") });
},
setupController(controller, model) {
controller.set('model', model);
controller.set('user', this.modelFor('user'));
controller.set("model", model);
controller.set("user", this.modelFor("user"));
this.viewingActionType(-1);
}
});

View File

@ -9,16 +9,16 @@ export default Discourse.Route.extend({
refreshQueryWithoutTransition: true,
titleToken() {
return I18n.t('directory.title');
return I18n.t("directory.title");
},
resetController(controller, isExiting) {
if (isExiting) {
controller.setProperties({
period: 'weekly',
order: 'likes_received',
period: "weekly",
order: "likes_received",
asc: null,
name: ''
name: ""
});
}
},
@ -26,11 +26,18 @@ export default Discourse.Route.extend({
model(params) {
// If we refresh via `refreshModel` set the old model to loading
this._params = params;
return this.store.find('directoryItem', params);
return this.store.find("directoryItem", params);
},
setupController(controller, model) {
const params = this._params;
controller.setProperties({ model, period: params.period, nameInput: params.name });
},
actions: {
didTransition() {
this.controllerFor("users")._showFooter();
return true;
}
}
});

View File

@ -1,6 +1,10 @@
{{render "header"}}
<div id='main-outlet' class='wrap'>
<div id="main-outlet" class="wrap">
<div class="container">
{{custom-html "top"}}
{{global-notice}}
</div>
{{outlet}}
{{render "user-card"}}
</div>

View File

@ -1,3 +1,3 @@
{{icon-or-image badge.icon}}
{{badge.displayName}}
<span class="badge-display-name">{{badge.displayName}}</span>
{{yield}}

View File

@ -0,0 +1,20 @@
{{#if isNotSupported}}
{{d-button icon="bell-slash" label="user.desktop_notifications.not_supported" disabled="true"}}
{{/if}}
{{#if isDefaultPermission}}
{{d-button icon="bell-slash" label="user.desktop_notifications.perm_default" action="requestPermission"}}
{{/if}}
{{#if isDeniedPermission}}
{{d-button icon="bell-slash" label="user.desktop_notifications.perm_denied_btn" action="recheckPermission"}}
{{i18n "user.desktop_notifications.perm_denied_expl"}}
{{/if}}
{{#if isGrantedPermission}}
{{#if isEnabled}}
{{d-button icon="bell-slash-o" label="user.desktop_notifications.disable" action="turnoff"}}
{{i18n "user.desktop_notifications.currently_enabled"}}
{{else}}
{{d-button icon="bell-o" label="user.desktop_notifications.enable" action="turnon"}}
{{i18n "user.desktop_notifications.currently_disabled"}}
{{/if}}
{{/if}}

View File

@ -2,7 +2,7 @@
<li>
<a class='search-link' href='{{unbound result.url}}'>
<span class='topic'>
{{topic-status topic=result.topic disableActions=true}}<span class='topic-title'>{{unbound result.topic.title}}</span>{{category-badge result.topic.category}}
{{topic-status topic=result.topic disableActions=true}}<span class='topic-title'>{{unbound result.topic.title}}</span>{{category-badge result.topic.category}}{{plugin-outlet "search-category"}}
</span>
{{#unless site.mobileView}}
<span class='blurb'>

View File

@ -11,5 +11,8 @@
{{avatar post imageSize="small"}}
</a>
{{/if}}
{{{description}}}
<p>{{description}}</p>
{{#if post.cooked}}
<div class='custom-message'>{{{post.cooked}}}</div>
{{/if}}
</div>

View File

@ -0,0 +1,31 @@
<div class='clearfix info'>
<a href={{item.userUrl}} data-user-card={{item.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
<span class='time'>{{format-date item.created_at}}</span>
{{topic-status topic=item disableActions=true}}
<span class="title">
<a href={{item.postUrl}}>{{{item.title}}}</a>
</span>
<div class="category">{{category-link item.category}}</div>
</div>
{{#if actionDescription}}
<p class='excerpt'>{{actionDescription}}</p>
{{/if}}
<p class='excerpt'>{{{item.excerpt}}}</p>
{{#each item.children as |child|}}
<div class='child-actions'>
<i class="icon {{child.icon}}"></i>
{{#each child.items as |grandChild|}}
{{#if grandChild.removableBookmark}}
<button class="btn btn-default remove-bookmark" {{action "removeBookmark" grandChild}}>
{{fa-icon 'times'}} {{i18n "bookmarks.remove"}}
</button>
{{else}}
<a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
{{#if grandChild.edit_reason}} &mdash; <span class="edit-reason">{{grandChild.edit_reason}}</span>{{/if}}
{{/if}}
{{/each}}
</div>
{{/each}}

View File

@ -1,10 +1,8 @@
<div class='container'>
{{custom-html "top"}}
{{global-notice}}
<div class="container">
{{discourse-banner user=currentUser banner=site.banner}}
</div>
<div class='list-controls'>
<div class="list-controls">
<div class="container">
{{outlet "navigation-bar"}}
</div>
@ -12,17 +10,17 @@
{{conditional-loading-spinner condition=loading}}
<div class="container list-container {{if loading 'hidden'}}">
<div class="container list-container {{if loading "hidden"}}">
<div class="row">
<div class="full-width">
<div id='header-list-area'>
<div id="header-list-area">
{{outlet "header-list-container"}}
</div>
</div>
</div>
<div class="row">
<div class="full-width">
<div id='list-area'>
<div id="list-area">
{{plugin-outlet "discovery-list-container-top"}}
{{outlet "list-container"}}
</div>

View File

@ -1,45 +1,49 @@
<div class="search row">
{{input type="text" value=searchTerm class="input-xxlarge search no-blur" action="search"}}
<button {{action "search"}} class="btn btn-primary"><i class='fa fa-search'></i></button>
{{d-button action="search" icon="search" class="btn-primary"}}
</div>
{{#conditional-loading-spinner condition=loading}}
{{#unless model.posts}}
<h3>{{i18n "search.no_results"}} <a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "search.search_help"}}</a>
</h3>
{{/unless}}
{{#unless model.posts}}
<h3>
{{i18n "search.no_results"}} <a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "search.search_help"}}</a>
</h3>
{{/unless}}
{{#each model.posts as |result|}}
<div class='fps-result'>
<div class='topic'>
{{avatar result imageSize="tiny"}}
<a class='search-link' href='{{unbound result.url}}'>
{{topic-status topic=result.topic disableActions=true}}<span class='topic-title'>{{unbound result.topic.title}}</span>
</a>{{category-link result.topic.category}}
</div>
<div class='blurb container'>
<span class='date'>
{{format-age result.created_at}}
{{#if result.blurb}}
-
{{/if}}
</span>
{{#if result.blurb}}
{{#highlight-text highlight=controller.q}}
{{{unbound result.blurb}}}
{{/highlight-text}}
{{/if}}
</div>
</div>
{{/each}}
{{#each model.posts as |result|}}
<div class='fps-result'>
<div class='topic'>
{{avatar result imageSize="tiny"}}
<a class='search-link' href='{{unbound result.url}}'>
{{topic-status topic=result.topic disableActions=true}}<span class='topic-title'>{{unbound result.topic.title}}</span>
</a>
<div class='search-category'>
{{category-link result.topic.category}}
{{plugin-outlet "full-page-search-category"}}
</div>
</div>
<div class='blurb container'>
<span class='date'>
{{format-age result.created_at}}
{{#if result.blurb}}
-
{{/if}}
</span>
{{#if result.blurb}}
{{#highlight-text highlight=controller.q}}
{{{unbound result.blurb}}}
{{/highlight-text}}
{{/if}}
</div>
</div>
{{/each}}
{{#if model.posts}}
<h3 class="search-footer">
{{i18n "search.no_more_results"}}
<a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "search.search_help"}}</a>
</h3>
{{/if}}
{{#if model.posts}}
<h3 class="search-footer">
{{i18n "search.no_more_results"}}
<a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "search.search_help"}}</a>
</h3>
{{/if}}
{{/conditional-loading-spinner}}

View File

@ -10,6 +10,7 @@
{{#if controller.showTopicPostBadges}}
{{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}}
{{/if}}
{{plugin-outlet "topic-list-tags"}}
{{#if expandPinned}}
{{raw "list/topic-excerpt" topic=topic}}
{{/if}}

View File

@ -1,10 +1,4 @@
<div id='move-selected' class="modal-body">
{{#if error}}
<div class="alert alert-error">
<button class="close" data-dismiss="alert">×</button>
</div>
{{/if}}
<p>{{{i18n 'topic.merge_topic.instructions' count=selectedPostsCount}}}</p>
<form>
@ -13,5 +7,7 @@
</div>
<div class="modal-footer">
<button class='btn btn-primary' {{bind-attr disabled="buttonDisabled"}} {{action "movePostsToExistingTopic"}}><i class="fa fa-sign-out"></i>{{buttonTitle}}</button>
{{#d-button class="btn-primary" disabled=buttonDisabled action="movePostsToExistingTopic"}}
{{fa-icon 'sign-out'}} {{buttonTitle}}
{{/d-button}}
</div>

View File

@ -1,10 +1,4 @@
<div id='move-selected' class="modal-body">
{{#if error}}
<div class="alert alert-error">
<button class="close" data-dismiss="alert">×</button>
</div>
{{/if}}
{{{i18n 'topic.split_topic.instructions' count=selectedPostsCount}}}
<form>
@ -18,5 +12,7 @@
</div>
<div class="modal-footer">
<button class='btn btn-primary' {{bind-attr disabled="buttonDisabled"}} {{action "movePostsToNewTopic"}}><i class='fa fa-sign-out'></i>{{buttonTitle}}</button>
{{#d-button class="btn-primary" disabled=buttonDisabled action="movePostsToNewTopic"}}
{{fa-icon 'sign-out'}} {{buttonTitle}}
{{/d-button}}
</div>

View File

@ -6,7 +6,6 @@
{{#user-link user=ctrl.post.user}}
{{avatar ctrl.post.user imageSize="large"}}
{{/user-link}}
</div>
<div class='cooked'>
<div class='names'>
@ -14,6 +13,9 @@
{{#user-link user=ctrl.post.user}}
{{ctrl.post.user.username}}
{{/user-link}}
{{#if ctrl.post.user.blocked}}
<i class='fa fa-ban' title='{{i18n "user.blocked_tooltip"}}'></i>
{{/if}}
</span>
</div>
<div class='post-info'>

View File

@ -1,25 +1,23 @@
<div class='container'>
{{custom-html "top"}}
{{global-notice}}
{{#if model}}
{{#if model}}
<div class="container">
{{discourse-banner user=currentUser banner=site.banner overlay=view.hasScrolled hide=model.errorLoading}}
{{/if}}
</div>
</div>
{{/if}}
{{plugin-outlet "topic-above-post-stream"}}
{{#if model.postStream.loaded}}
{{#if model.postStream.firstPostPresent}}
<div id='topic-title'>
<div class='container'>
<div id="topic-title">
<div class="container">
<div class="title-wrapper">
{{#if editingTopic}}
{{#if model.isPrivateMessage}}
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
{{autofocus-text-field id='edit-title' value=buffered.title maxLength=maxTitleLength}}
{{autofocus-text-field id="edit-title" value=buffered.title maxLength=maxTitleLength}}
{{else}}
{{autofocus-text-field id='edit-title' value=buffered.title maxLength=maxTitleLength}}
{{autofocus-text-field id="edit-title" value=buffered.title maxLength=maxTitleLength}}
<br>
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
{{/if}}
@ -36,13 +34,13 @@
{{#if model.details.loaded}}
{{topic-status topic=model}}
<a href='{{unbound model.url}}' {{action "jumpTop"}} class='fancy-title'>
<a href="{{unbound model.url}}" {{action "jumpTop"}} class="fancy-title">
{{{model.fancyTitle}}}
</a>
{{/if}}
{{#if model.details.can_edit}}
<a href {{action "editTopic"}} class='edit-topic' title='{{i18n 'edit'}}'>{{fa-icon "pencil"}}</a>
<a href {{action "editTopic"}} class="edit-topic" title="{{i18n "edit"}}">{{fa-icon "pencil"}}</a>
{{/if}}
</h1>
@ -61,10 +59,10 @@
{{view "selected-posts"}}
<div class="row">
<section class="topic-area" id='topic' data-topic-id='{{unbound model.id}}'>
<div class='posts-wrapper'>
<section class="topic-area" id="topic" data-topic-id="{{unbound model.id}}">
<div class="posts-wrapper">
{{render 'topic-progress'}}
{{render "topic-progress"}}
{{conditional-loading-spinner condition=model.postStream.loadingAbove}}
@ -83,21 +81,21 @@
{{conditional-loading-spinner condition=model.postStream.loadingBelow}}
</div>
<div id='topic-bottom'></div>
<div id="topic-bottom"></div>
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
{{#if loadedAllPosts}}
{{view 'topic-closing' topic=model}}
{{view 'topic-footer-buttons' topic=model}}
{{view "topic-closing" topic=model}}
{{view "topic-footer-buttons" topic=model}}
{{#if model.pending_posts_count}}
<div class='has-pending-posts'>
<div class="has-pending-posts">
{{{i18n "queue.has_pending_posts" count=model.pending_posts_count}}}
{{#link-to 'queued-posts'}}
{{fa-icon 'check'}}
{{i18n 'queue.view_pending'}}
{{#link-to "queued-posts"}}
{{fa-icon "check"}}
{{i18n "queue.view_pending"}}
{{/link-to}}
</div>
{{/if}}
@ -105,9 +103,9 @@
{{plugin-outlet "topic-above-suggested"}}
{{#if model.details.suggested_topics.length}}
<div id='suggested-topics'>
<h3>{{i18n 'suggested_topics.title'}}</h3>
<div class='topics'>
<div id="suggested-topics">
<h3>{{i18n "suggested_topics.title"}}</h3>
<div class="topics">
{{basic-topic-list topics=model.details.suggested_topics postsAction="showTopicEntrance"}}
</div>
<h3>{{{view.browseMoreMessage}}}</h3>
@ -122,10 +120,10 @@
</div>
{{else}}
<div class='container'>
<div class="container">
{{#conditional-loading-spinner condition=noErrorYet}}
{{#if model.notFoundHtml}}
<div class='not-found'>{{{model.notFoundHtml}}}</div>
<div class="not-found">{{{model.notFoundHtml}}}</div>
{{else}}
<div class="topic-error">
<div>{{model.message}}</div>

View File

@ -76,7 +76,7 @@
{{#if invite.reinvited}}
{{i18n 'user.invited.reinvited'}}
{{else}}
{{d-button icon="user-plus" action="reinvite" actionParam=invite class="btn" label="user.invited.reinvite"}}
{{d-button icon="refresh" action="reinvite" actionParam=invite class="btn" label="user.invited.reinvite"}}
{{/if}}
</td>
{{/if}}

View File

@ -189,6 +189,12 @@
</div>
</div>
<div class="control-group notifications">
<label class="control-label">{{i18n 'user.desktop_notifications.label'}}</label>
{{desktop-notification-config}}
<div class="instructions">{{i18n 'user.desktop_notifications.each_browser_note'}}</div>
</div>
<div class="control-group other">
<label class="control-label">{{i18n 'user.other_settings'}}</label>

View File

@ -1,29 +1,3 @@
{{#each item in model.content}}
<div {{bind-attr class=":item item.hidden item.deleted item.moderator_action"}}>
<div class='clearfix info'>
<a href="{{unbound item.userUrl}}" data-user-card="{{unbound item.username}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
<span class='time'>{{format-date item.created_at}}</span>
{{topic-status topic=item disableActions=true}}
<span class="title">
<a href="{{unbound item.postUrl}}">{{{unbound item.title}}}</a>
</span>
<div class="category">{{category-link item.category}}</div>
</div>
<p class='excerpt'>{{{unbound item.excerpt}}}</p>
{{#each child in item.children}}
<div class='child-actions'>
<i class="icon {{unbound child.icon}}"></i>
{{#each grandChild in child.items}}
{{#if grandChild.removableBookmark}}
<button class="btn btn-default remove-bookmark" {{action "removeBookmark" grandChild}}>
{{fa-icon 'times'}} {{i18n "bookmarks.remove"}}
</button>
{{else}}
<a href="{{unbound grandChild.userUrl}}" data-user-card="{{unbound grandChild.username}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
{{#if grandChild.edit_reason}} &mdash; <span class="edit-reason">{{unbound grandChild.edit_reason}}</span>{{/if}}
{{/if}}
{{/each}}
</div>
{{/each}}
</div>
{{#each model.content as |item|}}
{{stream-item item=item removeBookmark="removeBookmark"}}
{{/each}}

View File

@ -1,8 +1,3 @@
<div class='container'>
{{custom-html "top"}}
{{global-notice}}
</div>
<div class="container">
<section class='user-main'>
<section {{bind-attr class="collapsedInfo :about model.profileBackground:has-background:no-background"}} style={{model.profileBackground}}>

View File

@ -85,6 +85,8 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
const controller = this.get('controller');
controller.checkReplyLength();
this.get('controller.model').typing();
const lastKeyUp = new Date();
this.set('lastKeyUp', lastKeyUp);
@ -258,9 +260,9 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
this.editor = editor = Discourse.Markdown.createEditor({
containerElement: this.element,
lookupAvatarByPostNumber(postNumber) {
lookupAvatarByPostNumber(postNumber, topicId) {
const posts = self.get('controller.controllers.topic.model.postStream.posts');
if (posts) {
if (posts && topicId === self.get('controller.controllers.topic.model.id')) {
const quotedPost = posts.findProperty("post_number", postNumber);
if (quotedPost) {
const username = quotedPost.get('username'),
@ -543,8 +545,11 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
this.$('.wmd-preview').off('click.preview');
const self = this;
Em.run.next(() => {
$('#main-outlet').css('padding-bottom', 0);
const sizePx = self.get('composeState') === Discourse.Composer.CLOSED ? 0 : $('#reply-control').height();
$('#main-outlet').css('padding-bottom', sizePx);
// need to wait a bit for the "slide down" transition of the composer
Em.run.later(() => {
this.appEvents.trigger("composer:closed");

View File

@ -1,5 +1,3 @@
import ScrollTop from 'discourse/mixins/scroll-top';
import ScrollTop from "discourse/mixins/scroll-top";
export default Ember.View.extend(ScrollTop, {
});
export default Ember.View.extend(ScrollTop, {});

View File

@ -2,6 +2,6 @@ import SelectedPostsCount from 'discourse/mixins/selected-posts-count';
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend(SelectedPostsCount, {
templateName: 'modal/split_topic',
templateName: 'modal/split-topic',
title: I18n.t('topic.split_topic.title')
});

View File

@ -0,0 +1,27 @@
import LoadMore from "discourse/mixins/load-more";
export default Ember.View.extend(LoadMore, {
loading: false,
eyelineSelector: '.user-stream .item',
classNames: ['user-stream'],
_scrollTopOnModelChange: function() {
Em.run.schedule('afterRender', function() {
$(document).scrollTop(0);
});
}.observes('controller.model.user.id'),
actions: {
loadMore() {
const self = this;
if (this.get('loading')) { return; }
this.set('loading', true);
const stream = this.get('controller.model');
stream.findItems().then(function() {
self.set('loading', false);
self.get('eyeline').flushRest();
});
}
}
});

View File

@ -12,12 +12,12 @@ export default Ember.View.extend(LoadMore, {
}.observes('controller.model.user.id'),
actions: {
loadMore: function() {
var self = this;
loadMore() {
const self = this;
if (this.get('loading')) { return; }
this.set('loading', true);
var stream = this.get('controller.model');
const stream = this.get('controller.model');
stream.findItems().then(function() {
self.set('loading', false);
self.get('eyeline').flushRest();

View File

@ -28,6 +28,8 @@
//= require_tree ./discourse/adapters
//= require ./discourse/models/rest
//= require ./discourse/models/model
//= require ./discourse/models/result-set
//= require ./discourse/models/store
//= require ./discourse/models/post-action-type
//= require ./discourse/models/action-summary
//= require ./discourse/models/post

View File

@ -3,6 +3,24 @@
@import "common/foundation/mixins";
@import "common/foundation/helpers";
$mobile-breakpoint: 700px;
// Change the box model for .admin-content
@media (max-width: $mobile-breakpoint) {
.admin-content {
box-sizing: border-box;
*, *:before, *:after {
box-sizing: inherit;
}
input[type="text"] {
// Desktop/_discourse.scss sets a height on text-input elements. Using `box-sizing: border-box`
// this value either needs to be increased or set to auto. `mobile.css` seems to not set a height on text-inputs.
height: auto;
}
}
}
.admin-contents table {
width: 100%;
tr {text-align: left;}
@ -32,7 +50,7 @@ td.flaggers td {
.admin-content {
margin-bottom: 50px;
.admin-contents {
padding: 8px;
padding: 8px 0;
@include clearfix();
}
@ -96,6 +114,10 @@ td.flaggers td {
margin-top: 20px;
}
.admin-container .controls {
@include clearfix;
}
.admin-title {
height: 45px;
}
@ -103,7 +125,7 @@ td.flaggers td {
.admin-controls {
background-color: dark-light-diff($primary, $secondary, 90%, -75%);
padding: 10px 10px 3px 0;
height: 35px;
@include clearfix;
.nav.nav-pills {
li.active {
a {
@ -147,6 +169,14 @@ td.flaggers td {
label {
margin-top: 5px;
}
.controls {
margin-left: 0;
}
// Hide the search checkbox for very small screens
// Todo: find somewhere to display it - probably requires switching its order in the html
@media (max-width: 450px) {
display: none;
}
}
.toggle {
margin-top: 8px;
@ -184,6 +214,9 @@ td.flaggers td {
.admin-nav {
width: 18.018%;
@media (max-width: $mobile-breakpoint) {
width: 33%;
}
margin-top: 30px;
.nav-stacked {
border-right: none;
@ -196,10 +229,16 @@ td.flaggers td {
.admin-detail {
width: 76.5765%;
@media (max-width: $mobile-breakpoint) {
width: 67%;
}
min-height: 800px;
margin-left: 0;
border-left: solid 1px dark-light-diff($primary, $secondary, 90%, -60%);
padding: 30px 0 30px 30px;
@media (max-width: $mobile-breakpoint) {
padding: 30px 0 30px 16px;
}
}
.settings {
@ -210,13 +249,27 @@ td.flaggers td {
float: left;
width: 17.6576%;
margin-right: 12px;
@media (max-width: $mobile-breakpoint) {
float: none;
margin-right: 0;
width: 100%;
h3 {
margin-bottom: 6px;
}
}
}
.setting-value {
float: left;
width: 53%;
.select2-container {
@media (max-width: $mobile-breakpoint) {
width: 100%;
}
.select2-container {
width: 100% !important; // Needs !important to override hard-coded value
@media (max-width: $mobile-breakpoint) {
width: 100% !important; // !important overrides hard-coded mobile width of 68px
}
}
.select2-container-multi .select2-choices {
border: none;
}
@ -227,10 +280,15 @@ td.flaggers td {
.input-setting-string {
width: 404px;
@include medium-width { width: 314px; }
@include small-width { width: 284px; }
@media (max-width: $mobile-breakpoint) {
width: 100%;
}
}
.input-setting-list {
width: 408px;
@media (max-width: $mobile-breakpoint) {
width: 100%;
}
padding: 1px;
background-color: $secondary;
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
@ -255,7 +313,7 @@ td.flaggers td {
border-radius: 3px;
background-clip: padding-box;
-moz-user-select: none;
background-color: none;
background-color: transparent;
width: 3em;
height: 1em;
}
@ -553,6 +611,8 @@ section.details {
.style-name {
width: 350px;
height: 25px;
// Remove height to for `box-sizing: border-box`
height: auto;
}
.ace-wrapper {
position: relative;
@ -698,10 +758,13 @@ section.details {
.version-check {
th {
text-align: left !important;
}
.version-number {
font-size: 1.286em;
font-weight: bold;
text-align: center;
}
.face {
@ -1143,6 +1206,7 @@ table.api-keys {
.staff-actions {
width: 100%;
min-width: 990px;
.action {
width: 10.810%;
}
@ -1532,3 +1596,19 @@ table#user-badges {
.permalink-title {
margin-bottom: 10px;
}
// Mobile specific styles
// Mobile view text-inputs need some padding
.mobile-view .admin-contents {
input[type="text"] {
padding: 4px;
}
}
.mobile-view .admin-controls {
padding: 10px 10px 9px 0;
}
.mobile-view .full-width {
margin: 0;
}

View File

@ -4,13 +4,14 @@
max-width: inherit;
}
.search-category {
margin-top: 3px;
}
margin-bottom: 28px;
max-width: 675px;
.topic {
a {
color: scale-color($primary, $lightness: 10%);
}
line-height: 20px;
padding-bottom: 2px;
}
.avatar {
position: relative;
@ -19,15 +20,15 @@
}
.search-link {
.topic-statuses, .topic-title {
font-size: 1.4em;
font-size: 1.25em;
}
}
.blurb {
font-size: 1.0em;
line-height: 24px;
line-height: 20px;
word-wrap: break-word;
clear: both;
color: scale-color($primary, $lightness: 20%);
color: scale-color($primary, $lightness: 40%);
.date {
color: scale-color($primary, $lightness: 40%);
}

View File

@ -51,7 +51,9 @@
}
img {
display: inline-block;
display: block;
margin: auto;
margin-bottom: 4px;
width: 55px;
height: 55px;
}

View File

@ -128,8 +128,7 @@
&:before {
margin-right: 9px;
font-family: FontAwesome;
line-height: 1.6em;
font-size: 1.3em;
font-size: 17px;
}
&.google, &.google_oauth2 {
background: $google;

View File

@ -168,12 +168,14 @@
th {
text-align: left;
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
padding: 5px;
border-bottom: 3px solid dark-light-diff($primary, $secondary, 90%, -60%);
padding: 0 0 10px 0;
color: scale-color($primary, $lightness: 50%);
font-weight: normal;
}
td {
padding: 5px;
padding: 10px 0 10px 0;
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
}
}

View File

@ -50,6 +50,7 @@ article.post {
img {
max-width: 45px;
border-radius: 50%;
}
}

View File

@ -514,6 +514,9 @@ span.highlighted {
}
.small-action .small-action-desc {
p {
padding-top: 0;
}
.custom-message {
margin-left: -40px;
}
@ -521,6 +524,7 @@ span.highlighted {
.small-action .topic-avatar {
padding: 0;
margin: 0;
}
.small-action.time-gap .topic-avatar {

View File

@ -88,7 +88,18 @@ class PostsController < ApplicationController
end
def create
if !is_api? && current_user.blocked?
# error has parity with what user would get if they posted when blocked
# and it went through post creator
render json: {errors: [I18n.t("topic_not_found")]}, status: 422
return
end
@manager_params = create_params
@manager_params[:first_post_checks] = !is_api?
manager = NewPostManager.new(current_user, @manager_params)
if is_api?
@ -353,7 +364,7 @@ class PostsController < ApplicationController
# If a param is present it uses that result structure.
def backwards_compatible_json(json_obj, success)
json_obj.symbolize_keys!
if params[:nested_post].blank? && json_obj[:errors].blank?
if params[:nested_post].blank? && json_obj[:errors].blank? && json_obj[:action] != :enqueued
json_obj = json_obj[:post]
end
@ -403,7 +414,6 @@ class PostsController < ApplicationController
# Awful hack, but you can't seem to remove the `default_scope` when joining
# So instead I grab the topics separately
topic_ids = posts.dup.pluck(:topic_id)
secured_category_ids = guardian.secure_category_ids
topics = Topic.where(id: topic_ids).with_deleted.where.not(archetype: 'private_message')
topics = topics.secured(guardian)
@ -422,7 +432,9 @@ class PostsController < ApplicationController
:category,
:target_usernames,
:reply_to_post_number,
:auto_track
:auto_track,
:typing_duration_msecs,
:composer_open_duration_msecs
]
# param munging for WordPress

View File

@ -24,7 +24,7 @@ class UserActionsController < ApplicationController
UserAction.stream(opts)
end
render_serialized(stream, UserActionSerializer, root: "user_actions")
render_serialized(stream, UserActionSerializer, root: 'user_actions')
end
def show

View File

@ -31,6 +31,9 @@ module Jobs
end
def handle_failure(mail_string, e)
Rails.logger.warn("Email can not be processed: #{e}\n\n#{mail_string}") if SiteSetting.log_mail_processing_failures
template_args = {}
case e
when Email::Receiver::UserNotSufficientTrustLevelError

View File

@ -45,6 +45,8 @@ module HasCustomFields
has_many :_custom_fields, dependent: :destroy, :class_name => "#{name}CustomField"
after_save :save_custom_fields
attr_accessor :preloaded_custom_fields
# To avoid n+1 queries, use this function to retrieve lots of custom fields in one go
# and create a "sideloaded" version for easy querying by id.
def self.custom_fields_for_ids(ids, whitelisted_fields)
@ -73,6 +75,39 @@ module HasCustomFields
@custom_field_types[name] = type
end
def self.preload_custom_fields(objects, fields)
if objects.present?
map = {}
empty = {}
fields.each do |field|
empty[field] = nil
end
objects.each do |obj|
map[obj.id] = obj
obj.preloaded_custom_fields = empty.dup
end
fk = (name.underscore << "_id")
"#{name}CustomField".constantize
.where("#{fk} in (?)", map.keys)
.where("name in (?)", fields)
.pluck(fk, :name, :value).each do |id, name, value|
preloaded = map[id].preloaded_custom_fields
if preloaded[name].nil?
preloaded.delete(name)
end
HasCustomFields::Helpers.append_field(preloaded, name, value, @custom_field_types)
end
end
end
end
def reload(options = nil)
@ -80,12 +115,36 @@ module HasCustomFields
super
end
def custom_field_preloaded?(name)
@preloaded_custom_fields && @preloaded_custom_fields.key?(name)
end
def clear_custom_fields
@custom_fields = nil
@custom_fields_orig = nil
end
class PreloadedProxy
def initialize(preloaded)
@preloaded = preloaded
end
def [](key)
if @preloaded.key?(key)
@preloaded[key]
else
# for now you can not mix preload an non preload, it better just to fail
raise StandardError, "Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries."
end
end
end
def custom_fields
if @preloaded_custom_fields
return @preloaded_proxy ||= PreloadedProxy.new(@preloaded_custom_fields)
end
@custom_fields ||= refresh_custom_fields_from_db.dup
end

View File

@ -7,7 +7,11 @@ class Draft < ActiveRecord::Base
d = find_draft(user,key)
if d
return if d.sequence > sequence
d.update_columns(data: data, sequence: sequence)
exec_sql("UPDATE drafts
SET data = :data,
sequence = :sequence,
revisions = revisions + 1
WHERE id = :id", id: d.id, sequence: sequence, data: data)
else
Draft.create!(user_id: user.id, draft_key: key, data: data, sequence: sequence)
end

View File

@ -37,6 +37,7 @@ class Post < ActiveRecord::Base
has_many :uploads, through: :post_uploads
has_one :post_search_data
has_one :post_stat
has_many :post_details

View File

@ -123,11 +123,15 @@ class PostMover
end
def create_moderator_post_in_original_topic
move_type_str = PostMover.move_types[@move_type].to_s
original_topic.add_moderator_post(
user,
I18n.t("move_posts.#{PostMover.move_types[@move_type]}_moderator_post",
I18n.t("move_posts.#{move_type_str}_moderator_post",
count: post_ids.count,
topic_link: "[#{destination_topic.title}](#{destination_topic.url})"),
topic_link: "[#{destination_topic.title}](#{destination_topic.relative_url})"),
post_type: Post.types[:small_action],
action_code: "split_topic",
post_number: @first_post_number_moved
)
end

3
app/models/post_stat.rb Normal file
View File

@ -0,0 +1,3 @@
class PostStat < ActiveRecord::Base
belongs_to :post
end

View File

@ -64,8 +64,17 @@ class QueuedPost < ActiveRecord::Base
QueuedPost.transaction do
change_to!(:approved, approved_by)
if user.blocked?
user.update_columns(blocked: false)
end
creator = PostCreator.new(user, create_options.merge(skip_validations: true))
created_post = creator.create
unless created_post && creator.errors.blank?
raise StandardError, "Failed to create post #{raw[0..100]} #{creator.errors}"
end
end
DiscourseEvent.trigger(:approved_post, self)

View File

@ -29,6 +29,15 @@ class TopicLinkClick < ActiveRecord::Base
urls << uri.path if uri.try(:host) == Discourse.current_hostname
urls << url.sub(/\?.*$/, '') if url.include?('?')
# add a cdn link
if uri && Discourse.asset_host.present?
cdn_uri = URI.parse(Discourse.asset_host) rescue nil
if cdn_uri && cdn_uri.hostname == uri.hostname && uri.path.starts_with?(cdn_uri.path)
is_cdn_link = true
urls << uri.path[(cdn_uri.path.length)..-1]
end
end
link = TopicLink.select([:id, :user_id])
# test for all possible URLs
@ -54,7 +63,9 @@ class TopicLinkClick < ActiveRecord::Base
return nil unless uri
# Only redirect to whitelisted hostnames
return WHITELISTED_REDIRECT_HOSTNAMES.include?(uri.hostname) ? url : nil
return url if WHITELISTED_REDIRECT_HOSTNAMES.include?(uri.hostname) || is_cdn_link
return nil
end
return url if args[:user_id] && link.user_id == args[:user_id]

View File

@ -3,6 +3,9 @@ require_dependency 'avatar_lookup'
class TopicList
include ActiveModel::Serialization
cattr_accessor :preloaded_custom_fields
self.preloaded_custom_fields = []
attr_accessor :more_topics_url,
:prev_topics_url,
:draft,
@ -78,6 +81,10 @@ class TopicList
ft.topic_list = self
end
if TopicList.preloaded_custom_fields.present?
Topic.preload_custom_fields(@topics, TopicList.preloaded_custom_fields)
end
@topics
end

View File

@ -154,6 +154,7 @@ SQL
CASE WHEN coalesce(p.deleted_at, p2.deleted_at, t.deleted_at) IS NULL THEN false ELSE true END deleted,
p.hidden,
p.post_type,
p.action_code,
p.edit_reason,
t.category_id
FROM user_actions as a

View File

@ -29,11 +29,11 @@ class UserActionObserver < ActiveRecord::Observer
return unless action && post && user && post.id
row = {
action_type: action,
user_id: user.id,
acting_user_id: acting_user_id || post.user_id,
target_topic_id: post.topic_id,
target_post_id: post.id
action_type: action,
user_id: user.id,
acting_user_id: acting_user_id || post.user_id,
target_topic_id: post.topic_id,
target_post_id: post.id
}
if post.deleted_at.nil?
@ -48,12 +48,12 @@ class UserActionObserver < ActiveRecord::Observer
return if model.is_first_post?
row = {
action_type: UserAction::REPLY,
user_id: model.user_id,
acting_user_id: model.user_id,
target_post_id: model.id,
target_topic_id: model.topic_id,
created_at: model.created_at
action_type: UserAction::REPLY,
user_id: model.user_id,
acting_user_id: model.user_id,
target_post_id: model.id,
target_topic_id: model.topic_id,
created_at: model.created_at
}
rows = [row]
@ -79,12 +79,12 @@ class UserActionObserver < ActiveRecord::Observer
def log_topic(model)
row = {
action_type: model.archetype == Archetype.private_message ? UserAction::NEW_PRIVATE_MESSAGE : UserAction::NEW_TOPIC,
user_id: model.user_id,
acting_user_id: model.user_id,
target_topic_id: model.id,
target_post_id: -1,
created_at: model.created_at
action_type: model.archetype == Archetype.private_message ? UserAction::NEW_PRIVATE_MESSAGE : UserAction::NEW_TOPIC,
user_id: model.user_id,
acting_user_id: model.user_id,
target_topic_id: model.id,
target_post_id: -1,
created_at: model.created_at
}
rows = [row]

View File

@ -13,7 +13,7 @@ class QueuedPostSerializer < ApplicationSerializer
:category_id,
:can_delete_user
has_one :user, serializer: BasicUserSerializer
has_one :user, serializer: AdminUserListSerializer
has_one :topic, serializer: BasicTopicSerializer
def category_id

View File

@ -22,7 +22,8 @@ class UserActionSerializer < ApplicationSerializer
:title,
:deleted,
:hidden,
:moderator_action,
:post_type,
:action_code,
:edit_reason,
:category_id,
:uploaded_avatar_id,
@ -32,7 +33,7 @@ class UserActionSerializer < ApplicationSerializer
def excerpt
cooked = object.cooked || PrettyText.cook(object.raw)
PrettyText.excerpt(cooked, 300, { keep_emojis: true }) if cooked
PrettyText.excerpt(cooked, 300, keep_emojis: true) if cooked
end
def avatar_template
@ -67,10 +68,6 @@ class UserActionSerializer < ApplicationSerializer
object.title.present?
end
def moderator_action
object.post_type == Post.types[:moderator_action] || object.post_type == Post.types[:small_action]
end
def include_reply_to_post_number?
object.action_type == UserAction::REPLY
end

Some files were not shown because too many files have changed in this diff Show More