Version bump
This commit is contained in:
commit
3ba68857bd
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
<div class="container">
|
||||
{{global-notice}}
|
||||
<div class="row">
|
||||
<div class="full-width">
|
||||
|
||||
|
||||
@ -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" : "";
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,3 +1,5 @@
|
||||
/* You might be looking for navigation-item. */
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'li',
|
||||
classNameBindings: ['active'],
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
|
||||
@ -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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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" });
|
||||
}
|
||||
|
||||
@ -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}")
|
||||
|
||||
});
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -33,6 +33,7 @@ Discourse.Scrolling = Em.Mixin.create({
|
||||
}
|
||||
|
||||
Discourse.ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name);
|
||||
Em.run.scheduleOnce('afterRender', onScrollMethod);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import RestModel from 'discourse/models/rest';
|
||||
import Model from 'discourse/models/model';
|
||||
|
||||
|
||||
function topicsFrom(result, store) {
|
||||
if (!result) { return; }
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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" });
|
||||
}
|
||||
});
|
||||
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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');
|
||||
},
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
{{icon-or-image badge.icon}}
|
||||
{{badge.displayName}}
|
||||
<span class="badge-display-name">{{badge.displayName}}</span>
|
||||
{{yield}}
|
||||
|
||||
@ -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}}
|
||||
@ -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'>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}} — <span class="edit-reason">{{grandChild.edit_reason}}</span>{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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'>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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}} — <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}}
|
||||
|
||||
@ -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}}>
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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, {});
|
||||
|
||||
@ -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')
|
||||
});
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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%);
|
||||
}
|
||||
|
||||
@ -51,7 +51,9 @@
|
||||
}
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
margin: auto;
|
||||
margin-bottom: 4px;
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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%);
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ article.post {
|
||||
|
||||
img {
|
||||
max-width: 45px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
3
app/models/post_stat.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class PostStat < ActiveRecord::Base
|
||||
belongs_to :post
|
||||
end
|
||||
@ -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)
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
Reference in New Issue
Block a user