Version bump

This commit is contained in:
Neil Lalonde 2018-03-07 15:18:39 -05:00
commit edfd3967ab
674 changed files with 15819 additions and 9319 deletions

7
.gitattributes vendored
View File

@ -1,11 +1,14 @@
# Set default behaviour, in case users don't have core.autocrlf set.
* text=auto
# Explicitly declare text files we want to always be normalized and converted
# Treat email fixtures as binary files so CRLF are not converted to LF.
*.eml binary
# Explicitly declare text files we want to always be normalized and converted
# to native line endings on checkout.
*.yml text
# Custom for Visual Studio, very unlikely, but lets keep it
# Custom for Visual Studio, very unlikely, but lets keep it
*.cs diff=csharp
*.sln merge=union
*.csproj merge=union

View File

@ -1,11 +1,5 @@
# Install development dependencies on Mac OS X using Homebrew (http://mxcl.github.com/homebrew)
# add this repo to Homebrew's sources
tap 'homebrew/dupes'
# install the gcc compiler required for ruby
brew 'apple-gcc42'
# you probably already have git installed; ensure that it is the latest version
brew 'git'

18
Gemfile
View File

@ -2,8 +2,7 @@ source 'https://rubygems.org'
# if there is a super emergency and rubygems is playing up, try
#source 'http://production.cf.rubygems.org'
# does not install in linux ATM, so hack this for now
gem 'bootsnap', require: false
gem 'bootsnap', require: false, platform: :mri
def rails_master?
ENV["RAILS_MASTER"] == '1'
@ -36,7 +35,7 @@ gem 'redis-namespace'
gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox', '1.8.38'
gem 'onebox', '1.8.42'
gem 'http_accept_language', '~>2.0.5', require: false
@ -49,9 +48,10 @@ gem 'message_bus'
gem 'rails_multisite'
gem 'fast_xs'
gem 'fast_xs', platform: :mri
gem 'fast_xor'
# may move to xorcist post: https://github.com/fny/xorcist/issues/4
gem 'fast_xor', platform: :mri
gem 'fastimage'
@ -141,7 +141,7 @@ end
# this is an optional gem, it provides a high performance replacement
# to String#blank? a method that is called quite frequently in current
# ActiveRecord, this may change in the future
gem 'fast_blank'
gem 'fast_blank', platform: :mri
# this provides a very efficient lru cache
gem 'lru_redux'
@ -155,7 +155,7 @@ gem 'htmlentities', require: false
gem 'flamegraph', require: false
gem 'rack-mini-profiler', require: false
gem 'unicorn', require: false
gem 'unicorn', require: false, platform: :mri
gem 'puma', require: false
gem 'rbtrace', require: false, platform: :mri
gem 'gc_tracer', require: false, platform: :mri
@ -175,9 +175,13 @@ gem 'logster'
gem 'sassc', require: false
gem 'rotp'
gem 'rqrcode'
if ENV["IMPORT"] == "1"
gem 'mysql2'
gem 'redcarpet'
gem 'sqlite3', '~> 1.3.13'
gem 'ruby-bbcode-to-md', github: 'nlalonde/ruby-bbcode-to-md'
gem 'reverse_markdown'
end

View File

@ -63,9 +63,9 @@ GEM
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
binding_of_caller (0.7.2)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.0.0)
bootsnap (1.1.8)
msgpack (~> 1.0)
builder (3.2.3)
bullet (5.5.1)
@ -73,13 +73,14 @@ GEM
uniform_notifier (~> 1.10.0)
byebug (9.0.6)
certified (1.0.0)
chunky_png (1.3.8)
coderay (1.1.2)
concurrent-ruby (1.0.5)
connection_pool (2.2.1)
cppjieba_rb (0.3.0)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.2)
crass (1.0.3)
debug_inspector (0.0.3)
diff-lcs (1.3)
discourse-qunit-rails (0.0.11)
@ -183,14 +184,14 @@ GEM
metaclass (~> 0.0.1)
mock_redis (0.17.3)
moneta (1.0.0)
msgpack (1.1.0)
multi_json (1.12.1)
msgpack (1.2.4)
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
mustache (1.0.5)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
nokogumbo (1.4.13)
nokogumbo (1.5.0)
nokogiri
oauth (0.5.1)
oauth2 (1.3.1)
@ -199,7 +200,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.1.0)
oj (3.4.0)
omniauth (1.6.1)
hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3)
@ -228,8 +229,7 @@ GEM
omniauth-twitter (1.3.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.38)
fast_blank (>= 1.0.0)
onebox (1.8.42)
htmlentities (~> 4.3)
moneta (~> 1.0)
multi_json (~> 1.11)
@ -298,6 +298,9 @@ GEM
redis (~> 3.0, >= 3.0.4)
request_store (1.3.2)
rinku (2.0.2)
rotp (3.3.0)
rqrcode (0.10.1)
chunky_png (~> 1.0)
rspec (3.6.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
@ -338,10 +341,10 @@ GEM
nokogiri (>= 1.6.0)
ruby_dep (1.5.0)
safe_yaml (1.0.4)
sanitize (4.5.0)
sanitize (4.6.0)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
nokogumbo (~> 1.4)
sass (3.4.24)
sassc (1.11.2)
bundler
@ -461,7 +464,7 @@ DEPENDENCIES
omniauth-oauth2
omniauth-openid
omniauth-twitter
onebox (= 1.8.38)
onebox (= 1.8.42)
openid-redis-store
pg (~> 0.21.0)
pry-nav
@ -479,6 +482,8 @@ DEPENDENCIES
redis
redis-namespace
rinku
rotp
rqrcode
rspec
rspec-html-matchers
rspec-rails

View File

@ -57,7 +57,6 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https
## Contributing
[![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse)
[![Code Climate](https://codeclimate.com/github/discourse/discourse.svg)](https://codeclimate.com/github/discourse/discourse)
Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that
accepts contributions from the public &ndash; including you!

View File

@ -1,96 +1,10 @@
import BufferedContent from 'discourse/mixins/buffered-content';
import SiteSetting from 'admin/models/site-setting';
import { propertyNotEqual } from 'discourse/lib/computed';
import computed from 'ember-addons/ember-computed-decorators';
import { categoryLinkHTML } from 'discourse/helpers/category-link';
const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list', 'category_list', 'value_list'];
export default Ember.Component.extend(BufferedContent, {
classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
content: Ember.computed.alias('setting'),
dirty: propertyNotEqual('buffered.value', 'setting.value'),
validationMessage: null,
@computed("setting", "buffered.value")
preview(setting, value) {
// A bit hacky, but allows us to use helpers
if (setting.get('setting') === 'category_style') {
let category = this.site.get('categories.firstObject');
if (category) {
return categoryLinkHTML(category, {
categoryStyle: value
});
}
}
let preview = setting.get('preview');
if (preview) {
return new Handlebars.SafeString("<div class='preview'>" + preview.replace(/\{\{value\}\}/g, value) + "</div>");
}
},
@computed('componentType')
typeClass(componentType) {
return componentType.replace(/\_/g, '-');
},
@computed("setting.setting")
settingName(setting) {
return setting.replace(/\_/g, ' ');
},
@computed("setting.type")
componentType(type) {
return CustomTypes.indexOf(type) !== -1 ? type : 'string';
},
@computed("typeClass")
componentName(typeClass) {
return "site-settings/" + typeClass;
},
_watchEnterKey: function() {
const self = this;
this.$().on("keydown.site-setting-enter", ".input-setting-string", function (e) {
if (e.keyCode === 13) { // enter key
self._save();
}
});
}.on('didInsertElement'),
_removeBindings: function() {
this.$().off("keydown.site-setting-enter");
}.on("willDestroyElement"),
import SettingComponent from 'admin/mixins/setting-component';
export default Ember.Component.extend(BufferedContent, SettingComponent, {
_save() {
const setting = this.get('buffered'),
action = SiteSetting.update(setting.get('setting'), setting.get('value'));
action.then(() => {
this.set('validationMessage', null);
this.commitBuffer();
}).catch((e) => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
this.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
} else {
this.set('validationMessage', I18n.t('generic_error'));
}
});
},
actions: {
save() {
this._save();
},
resetDefault() {
this.set('buffered.value', this.get('setting.default'));
this._save();
},
cancel() {
this.rollbackBuffer();
}
const setting = this.get('buffered');
return SiteSetting.update(setting.get('setting'), setting.get('value'));
}
});

View File

@ -6,7 +6,7 @@ export default Ember.Component.extend({
enabled: {
get(value) {
if (Ember.isEmpty(value)) { return false; }
return value === "true";
return value.toString() === "true";
},
set(value) {
this.set("value", value ? "true" : "false");

View File

@ -0,0 +1,9 @@
import BufferedContent from 'discourse/mixins/buffered-content';
import SettingComponent from 'admin/mixins/setting-component';
export default Ember.Component.extend(BufferedContent, SettingComponent, {
layoutName: 'admin/templates/components/site-setting',
_save() {
return this.get('model').saveSettings(this.get('setting.setting'), this.get('buffered.value'));
}
});

View File

@ -6,11 +6,22 @@ export default Ember.Controller.extend({
section: null,
targets: [
{id: 0, name: I18n.t('admin.customize.theme.common')},
{id: 1, name: I18n.t('admin.customize.theme.desktop')},
{id: 2, name: I18n.t('admin.customize.theme.mobile')}
{ id: 0, name: 'common' },
{ id: 1, name: 'desktop' },
{ id: 2, name: 'mobile' },
{ id: 3, name: 'settings' }
],
fieldsForTarget: function (target) {
const common = ["scss", "head_tag", "header", "after_header", "body_tag", "footer"];
switch(target) {
case "common": return [...common, "embedded_scss"];
case "desktop": return common;
case "mobile": return common;
case "settings": return ["yaml"];
}
},
@computed('onlyOverridden')
showCommon() {
return this.shouldShow('common');
@ -26,6 +37,11 @@ export default Ember.Controller.extend({
return this.shouldShow('mobile');
},
@computed('onlyOverridden', 'model.remote_theme')
showSettings() {
return this.shouldShow('settings') && !this.get('model.remote_theme');
},
@observes('onlyOverridden')
onlyOverriddenChanged() {
if (this.get('onlyOverridden')) {
@ -51,27 +67,19 @@ export default Ember.Controller.extend({
currentTarget: 0,
setTargetName: function(name) {
let target;
switch(name) {
case "common": target = 0; break;
case "desktop": target = 1; break;
case "mobile": target = 2; break;
}
this.set("currentTarget", target);
const target = this.get('targets').find(t => t.name === name);
this.set("currentTarget", target && target.id);
},
@computed("currentTarget")
currentTargetName(target) {
switch(parseInt(target)) {
case 0: return "common";
case 1: return "desktop";
case 2: return "mobile";
}
currentTargetName(id) {
const target = this.get('targets').find(t => t.id === parseInt(id, 10));
return target && target.name;
},
@computed("fieldName")
activeSectionMode(fieldName) {
if (fieldName === "yaml") return "yaml";
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
},
@ -96,15 +104,9 @@ export default Ember.Controller.extend({
}
},
@computed("currentTarget", "onlyOverridden")
@computed("currentTargetName", "onlyOverridden")
fields(target, onlyOverridden) {
let fields = [
"scss", "head_tag", "header", "after_header", "body_tag", "footer"
];
if (parseInt(target) === 0) {
fields.push("embedded_scss");
}
let fields = this.fieldsForTarget(target);
if (onlyOverridden) {
const model = this.get("model");
@ -155,5 +157,4 @@ export default Ember.Controller.extend({
});
}
}
});

View File

@ -2,6 +2,7 @@ import { default as computed } from 'ember-addons/ember-computed-decorators';
import { url } from 'discourse/lib/computed';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import showModal from 'discourse/lib/show-modal';
import ThemeSettings from 'admin/models/theme-settings';
const THEME_UPLOAD_VAR = 2;
@ -30,7 +31,7 @@ export default Ember.Controller.extend({
return text + ": " + localized.join(" , ");
}
};
['common','desktop','mobile'].forEach(target=> {
['common', 'desktop', 'mobile'].forEach(target => {
descriptions.push(description(target));
});
return descriptions.reject(d=>Em.isBlank(d));
@ -77,6 +78,16 @@ export default Ember.Controller.extend({
return themes;
},
@computed("model.settings")
settings(settings) {
return settings.map(setting => ThemeSettings.create(setting));
},
@computed("settings")
hasSettings(settings) {
return settings.length > 0;
},
downloadUrl: url('model.id', '/admin/themes/%@'),
actions: {

View File

@ -19,6 +19,11 @@ export default Ember.Controller.extend(CanCheckEmails, {
primaryGroupDirty: propertyNotEqual('originalPrimaryGroupId', 'model.primary_group_id'),
canDisableSecondFactor: Ember.computed.and(
'model.second_factor_enabled',
'model.can_disable_second_factor'
),
automaticGroups: function() {
return this.get("model.automaticGroups").map((g) => g.name).join(", ");
}.property("model.automaticGroups"),
@ -62,7 +67,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
silence() { return this.get("model").silence(); },
deleteAllPosts() { return this.get("model").deleteAllPosts(); },
anonymize() { return this.get('model').anonymize(); },
destroy() { return this.get('model').destroy(); },
disableSecondFactor() { return this.get('model').disableSecondFactor(); },
destroy() {
const postCount = this.get('model.post_count');
if (postCount <= 5) {
return this.get('model').destroy({ deletePosts: true });
} else {
return this.get('model').destroy();
}
},
viewActionLogs() {
this.get('adminTools').showActionLogs(this, {

View File

@ -1,12 +1,13 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import { ajax } from 'discourse/lib/ajax';
// import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend(ModalFunctionality, {
local: Ember.computed.equal('selection', 'local'),
remote: Ember.computed.equal('selection', 'remote'),
selection: 'local',
adminCustomizeThemes: Ember.inject.controller(),
loading: false,
actions: {
importTheme() {
@ -24,11 +25,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
options.data = {remote: this.get('uploadUrl')};
}
this.set('loading', true);
ajax('/admin/themes/import', options).then(result=>{
const theme = this.store.createRecord('theme',result.theme);
this.get('adminCustomizeThemes').send('addTheme', theme);
this.send('closeModal');
});
}).catch(popupAjaxError).finally(() => this.set('loading', false));
}
}

View File

@ -0,0 +1,98 @@
import computed from 'ember-addons/ember-computed-decorators';
import { categoryLinkHTML } from 'discourse/helpers/category-link';
const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list', 'category_list', 'value_list'];
export default Ember.Mixin.create({
classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
content: Ember.computed.alias('setting'),
validationMessage: null,
@computed("buffered.value", "setting.value")
dirty(bufferVal, settingVal) {
if (bufferVal === null || bufferVal === undefined) bufferVal = '';
if (settingVal === null || settingVal === undefined) settingVal = '';
return bufferVal.toString() !== settingVal.toString();
},
@computed("setting", "buffered.value")
preview(setting, value) {
// A bit hacky, but allows us to use helpers
if (setting.get('setting') === 'category_style') {
let category = this.site.get('categories.firstObject');
if (category) {
return categoryLinkHTML(category, {
categoryStyle: value
});
}
}
let preview = setting.get('preview');
if (preview) {
return new Handlebars.SafeString("<div class='preview'>" + preview.replace(/\{\{value\}\}/g, value) + "</div>");
}
},
@computed('componentType')
typeClass(componentType) {
return componentType.replace(/\_/g, '-');
},
@computed("setting.setting")
settingName(setting) {
return setting.replace(/\_/g, ' ');
},
@computed("setting.type")
componentType(type) {
return CustomTypes.indexOf(type) !== -1 ? type : 'string';
},
@computed("typeClass")
componentName(typeClass) {
return "site-settings/" + typeClass;
},
_watchEnterKey: function() {
const self = this;
this.$().on("keydown.setting-enter", ".input-setting-string", function (e) {
if (e.keyCode === 13) { // enter key
self._save();
}
});
}.on('didInsertElement'),
_removeBindings: function() {
this.$().off("keydown.setting-enter");
}.on("willDestroyElement"),
_save() {
Em.warn("You should define a `_save` method", { id: "admin.mixins.setting-component" });
return Ember.RSVP.resolve();
},
actions: {
save() {
this._save().then(() => {
this.set('validationMessage', null);
this.commitBuffer();
}).catch(e => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
this.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
} else {
this.set('validationMessage', I18n.t('generic_error'));
}
});
},
resetDefault() {
this.set('buffered.value', this.get('setting.default'));
this._save();
},
cancel() {
this.rollbackBuffer();
}
}
});

View File

@ -0,0 +1,29 @@
export default Ember.Mixin.create({
overridden: function() {
let val = this.get('value'),
defaultVal = this.get('default');
if (val === null) val = '';
if (defaultVal === null) defaultVal = '';
return val.toString() !== defaultVal.toString();
}.property('value', 'default'),
validValues: function() {
const vals = [],
translateNames = this.get('translate_names');
this.get('valid_values').forEach(v => {
if (v.name && v.name.length > 0 && translateNames) {
vals.addObject({ name: I18n.t(v.name), value: v.value });
} else {
vals.addObject(v);
}
});
return vals;
}.property('valid_values'),
allowsNone: function() {
if ( _.indexOf(this.get('valid_values'), '') >= 0 ) return 'admin.settings.none';
}.property('valid_values')
});

View File

@ -168,6 +168,14 @@ const AdminUser = Discourse.User.extend({
}).catch(popupAjaxError);
},
disableSecondFactor() {
return ajax(`/admin/users/${this.get('id')}/disable_second_factor`, {
type: 'PUT'
}).then(() => {
this.set('second_factor_enabled', false);
}).catch(popupAjaxError);
},
refreshBrowsers() {
return ajax("/admin/users/" + this.get('id') + "/refresh_browsers", {
type: 'POST'

View File

@ -1,31 +1,7 @@
import { ajax } from 'discourse/lib/ajax';
const SiteSetting = Discourse.Model.extend({
overridden: function() {
let val = this.get('value'),
defaultVal = this.get('default');
import Setting from 'admin/mixins/setting-object';
if (val === null) val = '';
if (defaultVal === null) defaultVal = '';
return val.toString() !== defaultVal.toString();
}.property('value', 'default'),
validValues: function() {
const vals = [],
translateNames = this.get('translate_names');
this.get('valid_values').forEach(function(v) {
if (v.name && v.name.length > 0) {
vals.addObject(translateNames ? {name: I18n.t(v.name), value: v.value} : v);
}
});
return vals;
}.property('valid_values'),
allowsNone: function() {
if ( _.indexOf(this.get('valid_values'), '') >= 0 ) return 'admin.site_settings.none';
}.property('valid_values')
});
const SiteSetting = Discourse.Model.extend(Setting, {});
SiteSetting.reopenClass({
findAll() {

View File

@ -0,0 +1,3 @@
import Setting from 'admin/mixins/setting-object';
export default Discourse.Model.extend(Setting, {});

View File

@ -2,6 +2,7 @@ import RestModel from 'discourse/models/rest';
import { default as computed } from 'ember-addons/ember-computed-decorators';
const THEME_UPLOAD_VAR = 2;
const FIELDS_IDS = [0, 1, 5];
const Theme = RestModel.extend({
@ -14,13 +15,11 @@ const Theme = RestModel.extend({
}
let hash = {};
if (fields) {
fields.forEach(field=>{
if (!field.type_id || field.type_id < THEME_UPLOAD_VAR) {
hash[this.getKey(field)] = field;
}
});
}
fields.forEach(field => {
if (!field.type_id || FIELDS_IDS.includes(field.type_id)) {
hash[this.getKey(field)] = field;
}
});
return hash;
},
@ -29,11 +28,11 @@ const Theme = RestModel.extend({
if (!fields) {
return [];
}
return fields.filter((f)=> f.target === 'common' && f.type_id === THEME_UPLOAD_VAR);
return fields.filter(f => f.target === 'common' && f.type_id === THEME_UPLOAD_VAR);
},
getKey(field){
return field.target + " " + field.name;
return `${field.target} ${field.name}`;
},
hasEdited(target, name){
@ -151,6 +150,11 @@ const Theme = RestModel.extend({
.then(() => this.set("changed", false));
},
saveSettings(name, value) {
const settings = {};
settings[name] = value;
return this.save({ settings });
}
});
export default Theme;

View File

@ -18,6 +18,11 @@ export default Ember.Route.extend({
},
setupController(controller, wrapper) {
const fields = controller.fieldsForTarget(wrapper.target);
if (!fields.includes(wrapper.field_name)) {
this.transitionTo('adminCustomizeThemes.edit', wrapper.model.id, wrapper.target, fields[0]);
return;
}
controller.set("model", wrapper.model);
controller.setTargetName(wrapper.target || "common");
controller.set("fieldName", wrapper.field_name || "scss");

View File

@ -10,5 +10,5 @@
{{d-button class="cancel" action="cancel" icon="times"}}
</div>
{{else if setting.overridden}}
{{d-button action="resetDefault" icon="undo" label="admin.site_settings.reset"}}
{{d-button action="resetDefault" icon="undo" label="admin.settings.reset"}}
{{/if}}

View File

@ -22,7 +22,7 @@
<div class='search controls'>
<label>
{{input type="checkbox" checked=onlyOverridden}}
{{i18n 'admin.site_settings.show_overriden'}}
{{i18n 'admin.settings.show_overriden'}}
</label>
</div>
</div>

View File

@ -3,39 +3,47 @@
<h2>{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}</h2>
{{#if error}}
<pre class='field-error'>{{error}}</pre>
<pre class='field-error'>{{error}}</pre>
{{/if}}
<div class='edit-main-nav'>
<ul class='nav nav-pills target'>
{{#if showCommon}}
<li>
{{#link-to 'adminCustomizeThemes.edit' model.id 'common' fieldName replace=true title=field.title}}
{{i18n 'admin.customize.theme.common'}}
{{/link-to}}
</li>
<li>
{{#link-to 'adminCustomizeThemes.edit' model.id 'common' fieldName replace=true}}
{{i18n 'admin.customize.theme.common'}}
{{/link-to}}
</li>
{{/if}}
{{#if showDesktop}}
<li>
{{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true title=field.title}}
{{i18n 'admin.customize.theme.desktop'}}
{{d-icon 'desktop'}}
{{/link-to}}
</li>
<li>
{{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true}}
{{i18n 'admin.customize.theme.desktop'}}
{{d-icon 'desktop'}}
{{/link-to}}
</li>
{{/if}}
{{#if showMobile}}
<li class='mobile'>
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true title=field.title}}
{{i18n 'admin.customize.theme.mobile'}}
{{d-icon 'mobile'}}
{{/link-to}}
</li>
<li class='mobile'>
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true}}
{{i18n 'admin.customize.theme.mobile'}}
{{d-icon 'mobile'}}
{{/link-to}}
</li>
{{/if}}
{{#if showSettings}}
<li class='theme-settings'>
{{#link-to 'adminCustomizeThemes.edit' model.id 'settings' fieldName replace=true}}
{{i18n 'admin.customize.theme.settings'}}
{{d-icon 'cog'}}
{{/link-to}}
</li>
{{/if}}
</ul>
<div class='show-overidden'>
<label>
{{input type="checkbox" checked=onlyOverridden}}
{{i18n 'admin.site_settings.show_overriden'}}
{{i18n 'admin.settings.show_overriden'}}
</label>
</div>
<div class='clearfix'></div>

View File

@ -50,16 +50,16 @@
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
{{#if hasEditedFields}}
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
<ul>
{{#each editedDescriptions as |desc|}}
<li>{{desc}}</li>
{{/each}}
</ul>
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
<ul>
{{#each editedDescriptions as |desc|}}
<li>{{desc}}</li>
{{/each}}
</ul>
{{else}}
<p>
{{i18n "admin.customize.theme.edit_css_html_help"}}
</p>
<p>
{{i18n "admin.customize.theme.edit_css_html_help"}}
</p>
{{/if}}
<p>
{{#if model.remote_theme}}
@ -71,17 +71,17 @@
{{/if}}
{{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}}
{{#if model.remote_theme}}
<span class='status-message'>
{{#if updatingRemote}}
{{i18n 'admin.customize.theme.updating'}}
{{else}}
{{#if model.remote_theme.commits_behind}}
{{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}}
<span class='status-message'>
{{#if updatingRemote}}
{{i18n 'admin.customize.theme.updating'}}
{{else}}
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
{{#if model.remote_theme.commits_behind}}
{{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}}
{{else}}
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
{{/if}}
{{/if}}
{{/if}}
</span>
</span>
{{/if}}
</p>
@ -105,6 +105,15 @@
{{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
</p>
{{#if hasSettings}}
<h3>{{i18n "admin.customize.theme.theme_settings"}}</h3>
{{#d-section class="form-horizontal theme settings"}}
{{#each settings as |setting|}}
{{theme-setting setting=setting model=model class="theme-setting"}}
{{/each}}
{{/d-section}}
{{/if}}
{{#if availableChildThemes}}
<h3>{{i18n "admin.customize.theme.theme_components"}}</h3>
{{#unless model.childThemes.length}}

View File

@ -35,7 +35,7 @@
<label for="owner-selector">{{i18n 'admin.groups.add_owners'}}</label>
{{user-selector usernames=model.ownerUsernames
placeholderKey="admin.groups.selector_placeholder"
placeholderKey="groups.selector_placeholder"
id="owner-selector"}}
{{#if model.id}}

View File

@ -58,7 +58,7 @@
{{#unless item.editing}}
{{d-button action="destroy" actionParam=item icon="trash-o" class="btn-danger"}}
{{d-button action="edit" actionParam=item icon="pencil"}}
{{#if isBlocked}}
{{#if item.isBlocked}}
{{d-button action="allow" actionParam=item icon="check" label="admin.logs.screened_ips.actions.do_nothing"}}
{{else}}
{{d-button action="block" actionParam=item icon="ban" label="admin.logs.screened_ips.actions.block"}}

View File

@ -1,7 +1,7 @@
{{#if filteredContent}}
{{#d-section class="form-horizontal settings"}}
{{#each filteredContent as |setting|}}
{{site-setting setting=setting saveAction="saveSetting"}}
{{site-setting setting=setting}}
{{/each}}
{{/d-section}}
{{else}}

View File

@ -2,7 +2,7 @@
<div class='search controls'>
<label>
{{input type="checkbox" checked=onlyOverridden}}
{{i18n 'admin.site_settings.show_overriden'}}
{{i18n 'admin.settings.show_overriden'}}
</label>
</div>
<div class='controls'>

View File

@ -156,6 +156,22 @@
</div>
</div>
{{/if}}
<div class='display-row'>
<div class='field'>{{i18n 'user.second_factor.title'}}</div>
<div class='value'>
{{#if model.second_factor_enabled}}
{{i18n "yes_value"}}
{{else}}
{{i18n "no_value"}}
{{/if}}
</div>
<div class='controls'>
{{#if canDisableSecondFactor}}
{{d-button action="disableSecondFactor" icon="unlock-alt" label="user.second_factor.disable"}}
{{/if}}
</div>
</div>
</section>
{{#if userFields}}

View File

@ -98,6 +98,10 @@
{{#if user.moderator}}
{{d-icon "shield" title="admin.moderator" }}
{{/if}}
{{#if user.second_factor_enabled}}
{{d-icon "lock" title="admin.user.second_factor_enabled" }}
{{/if}}
</td>
</tr>
{{/each}}

View File

@ -10,6 +10,12 @@
{{watched-word-uploader uploading=uploading actionKey=actionNameKey done="uploadComplete"}}
<div class='clearfix'></div>
<div>
<label class="show-words-checkbox">
{{input type="checkbox" checked=adminWatchedWords.showWords disabled=adminWatchedWords.disableShowWords}}
{{i18n 'admin.watched_words.show_words'}}
</label>
</div>
<div class="watched-words-list">
{{#if showWordsList}}
{{#each filteredContent as |word| }}

View File

@ -1,10 +1,4 @@
<div class='admin-controls'>
<div class='search controls'>
<label class="show-words-checkbox">
{{input type="checkbox" checked=showWords disabled=disableShowWords}}
{{i18n 'admin.watched_words.show_words'}}
</label>
</div>
<div class='controls'>
{{text-field value=filter placeholderKey="admin.watched_words.search" class="no-blur"}}
{{d-button action="clearFilter" label="admin.watched_words.clear_filter"}}

View File

@ -11,6 +11,7 @@
// Stuff we need to load first
//= require ./discourse/lib/utilities
//= require ./discourse/lib/page-visible
//= require ./discourse/lib/logout
//= require ./discourse/lib/ajax
//= require ./discourse/lib/text
//= require ./discourse/lib/hash

View File

@ -61,5 +61,11 @@ export default Ember.Component.extend({
}
return false;
}
}
},
actions: {
showInserted() {
this.sendAction('showInserted');
},
},
});

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
classNames: ["categories-and-top"]
});

View File

@ -1,3 +1,3 @@
export default Ember.Component.extend({
classNames: ['latest-topic-list']
tagName: ''
});

View File

@ -38,11 +38,11 @@ export default Ember.Component.extend({
<a class="post-link" href="${postLink.href}">${postLink.anchor}</a>
${userAvatar}
<span class="username">${userLink.anchor}</span>
${iconHTML("mail-forward", { class: "reply-to-glyph" })}
`;
if (originalUser) {
editTitle += `
${iconHTML("mail-forward", { class: "reply-to-glyph" })}
${originalUser.avatar}
<span class="original-username">${originalUser.username}</span>
`;

View File

@ -372,7 +372,13 @@ export default Ember.Component.extend({
post.set('refreshedPost', true);
}
$oneboxes.each((_, o) => load({ elem: o, refresh, ajax, categoryId: this.get('composer.category.id') }));
$oneboxes.each((_, o) => load({
elem: o,
refresh,
ajax,
categoryId: this.get('composer.category.id'),
topicId: this.get('composer.topic.id')
}));
},
_warnMentionedGroups($preview) {

View File

@ -86,6 +86,7 @@ export default Ember.Component.extend({
ajax,
synchronous: true,
categoryId: this.get('composer.category.id'),
topicId: this.get('composer.topic.id')
});
if (loadOnebox && loadOnebox.then) {

View File

@ -14,11 +14,13 @@ export default Ember.Component.extend({
Ember.run.scheduleOnce('afterRender', this, this._afterFirstRender);
this.appEvents.on('modal-body:flash', msg => this._flash(msg));
this.appEvents.on('modal-body:clearFlash', () => this._clearFlash());
},
willDestroyElement() {
this._super();
this.appEvents.off('modal-body:flash');
this.appEvents.off('modal-body:clearFlash');
},
_afterFirstRender() {
@ -45,10 +47,16 @@ export default Ember.Component.extend({
);
},
_clearFlash() {
$('#modal-alert').hide().removeClass('alert-error', 'alert-success');
},
_flash(msg) {
$('#modal-alert').hide()
.removeClass('alert-error', 'alert-success')
.addClass(`alert alert-${msg.messageClass || 'success'}`).html(msg.text || '')
.fadeIn();
this._clearFlash();
$('#modal-alert')
.addClass(`alert alert-${msg.messageClass || 'success'}`)
.html(msg.text || '')
.fadeIn();
},
});

View File

@ -2,8 +2,7 @@ import { on, observes } from "ember-addons/ember-computed-decorators";
import { findRawTemplate } from "discourse/lib/raw-templates";
import { emojiUrlFor } from "discourse/lib/text";
import KeyValueStore from "discourse/lib/key-value-store";
import { emojis } from "pretty-text/emoji/data";
import { extendedEmojiList, isSkinTonableEmoji } from "pretty-text/emoji";
import { extendedEmojiList, isSkinTonableEmoji, emojiSearch } from "pretty-text/emoji";
const { run } = Ember;
const keyValueStore = new KeyValueStore("discourse_emojis_");
@ -205,10 +204,7 @@ export default Ember.Component.extend({
this.$list.css("visibility", "visible");
} else {
const lowerCaseFilter = this.get("filter").toLowerCase();
const filterableEmojis = emojis.concat(_.keys(extendedEmojiList()));
const filteredCodes = _.filter(filterableEmojis, code => {
return code.indexOf(lowerCaseFilter) > -1;
}).slice(0, 30);
const filteredCodes = emojiSearch(lowerCaseFilter, { maxResults: 30});
this.$results.empty().html(
_.map(filteredCodes, (code) => {
const hasDiversity = isSkinTonableEmoji(code);

View File

@ -13,8 +13,12 @@ export default Ember.Component.extend({
},
actions: {
externalLogin: function(provider) {
this.sendAction('action', provider);
emailLogin() {
this.sendAction('emailLogin');
},
externalLogin(provider) {
this.sendAction('externalLogin', provider);
}
}
});

View File

@ -11,7 +11,7 @@ export default Ember.Component.extend({
}
Ember.run.schedule('afterRender', () => {
$('#login-account-password, #login-account-name').keydown(e => {
$('#login-account-password, #login-account-name, #login-second-factor').keydown(e => {
if (e.keyCode === 13) {
this.sendAction();
}

View File

@ -31,7 +31,7 @@ export default Ember.Component.extend({
}
const unreadTopics = this.topicTrackingState.countUnread();
const newTopics = this.topicTrackingState.countNew();
const newTopics = this.currentUser ? this.topicTrackingState.countNew() : 0;
if (newTopics + unreadTopics > 0) {
const hasBoth = unreadTopics > 0 && newTopics > 0;

View File

@ -1,142 +0,0 @@
import renderTag from 'discourse/lib/render-tag';
function formatTag(t) {
return renderTag(t.id, {count: t.count, noHref: true});
}
export default Ember.TextField.extend({
classNameBindings: [':tag-chooser'],
attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'],
init() {
this._super();
const tags = this.get('tags') || [];
this.set('value', tags.join(", "));
if (this.get('allowCreate') !== false) {
this.set('allowCreate', this.site.get('can_create_tag'));
}
this.set('termMatchesForbidden', false);
},
_valueChanged: function() {
const tags = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq();
this.set('tags', tags);
}.observes('value'),
_tagsChanged: function() {
const $tagChooser = this.$(),
val = this.get('value');
if ($tagChooser && val !== this.get('tags')) {
if (this.get('tags')) {
const data = this.get('tags').map((t) => {return {id: t, text: t};});
$tagChooser.select2('data', data);
} else {
$tagChooser.select2('data', []);
}
}
}.observes('tags'),
didInsertElement() {
this._super();
const self = this;
const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
let limit = this.siteSettings.max_tags_per_topic;
if (this.get('unlimitedTagCount')) {
limit = null;
} else if (this.get('limit')) {
limit = parseInt(this.get('limit'));
}
this.$().select2({
tags: true,
placeholder: this.get('placeholder') === "" ? "" : I18n.t(this.get('placeholderKey') || 'tagging.choose_for_topic'),
maximumInputLength: this.siteSettings.max_tag_length,
maximumSelectionSize: limit,
width: this.get('width') || 'resolve',
initSelection(element, callback) {
const data = [];
function splitVal(string, separator) {
var val, i, l;
if (string === null || string.length < 1) return [];
val = string.split(separator);
for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
return val;
}
$(splitVal(element.val(), ",")).each(function () {
data.push({
id: this,
text: this
});
});
callback(data);
},
createSearchChoice(term, data) {
term = term.replace(filterRegexp, '').trim().toLowerCase();
// No empty terms, make sure the user has permission to create the tag
if (!term.length || !self.get('allowCreate') || self.get('termMatchesForbidden')) return;
if ($(data).filter(function() {
return this.text.localeCompare(term) === 0;
}).length === 0) {
return { id: term, text: term };
}
},
createSearchChoicePosition(list, item) {
// Search term goes on the bottom
list.push(item);
},
formatSelection(data) {
return data ? renderTag(this.text(data), {noHref: true}) : undefined;
},
formatSelectionCssClass() {
return "discourse-tag-select2";
},
formatResult: formatTag,
multiple: true,
ajax: {
quietMillis: 200,
cache: true,
url: Discourse.getURL("/tags/filter/search"),
dataType: 'json',
data: function (term) {
const selectedTags = self.get('tags');
const d = {
q: term,
limit: self.siteSettings.max_tag_search_results,
categoryId: self.get('categoryId')
};
if (selectedTags) {
d.selected_tags = selectedTags.slice(0,100);
}
if (!self.get('everyTag')) {
d.filterForInput = true;
}
return d;
},
results: function (data) {
if (self.siteSettings.tags_sort_alphabetically) {
data.results = data.results.sort(function(a,b) { return a.id > b.id; });
}
self.set('termMatchesForbidden', data.forbidden ? true : false);
return data;
}
},
});
},
willDestroyElement() {
this._super();
this.$().select2('destroy');
}
});

View File

@ -1,86 +0,0 @@
function renderTagGroup(tag) {
return "<a class='discourse-tag'>" + Handlebars.Utils.escapeExpression(tag.text ? tag.text : tag) + "</a>";
};
export default Ember.TextField.extend({
classNameBindings: [':tag-chooser'],
attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'],
_initValue: function() {
const names = this.get('tagGroups') || [];
this.set('value', names.join(","));
}.on('init'),
_valueChanged: function() {
const names = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq();
if ( this.get('tagGroups').join(',') !== this.get('value') ) {
this.set('tagGroups', names);
}
}.observes('value'),
_tagGroupsChanged: function() {
const $chooser = this.$(),
val = this.get('value');
if ($chooser && val !== this.get('tagGroups')) {
if (this.get('tagGroups')) {
const data = this.get('tagGroups').map((t) => {return {id: t, text: t};});
$chooser.select2('data', data);
} else {
$chooser.select2('data', []);
}
}
}.observes('tagGroups'),
_initializeChooser: function() {
const self = this;
this.$().select2({
tags: true,
placeholder: this.get('placeholderKey') ? I18n.t(this.get('placeholderKey')) : null,
initSelection(element, callback) {
const data = [];
function splitVal(string, separator) {
var val, i, l;
if (string === null || string.length < 1) return [];
val = string.split(separator);
for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
return val;
}
$(splitVal(element.val(), ",")).each(function () {
data.push({ id: this, text: this });
});
callback(data);
},
formatSelection: function (data) {
return data ? renderTagGroup(this.text(data)) : undefined;
},
formatSelectionCssClass: function(){
return "discourse-tag-select2";
},
formatResult: renderTagGroup,
multiple: true,
ajax: {
quietMillis: 200,
cache: true,
url: Discourse.getURL("/tag_groups/filter/search"),
dataType: 'json',
data: function (term) {
return { q: term, limit: self.siteSettings.max_tag_search_results };
},
results: function (data) {
data.results = data.results.sort(function(a,b) { return a.text > b.text; });
return data;
}
},
});
}.on('didInsertElement'),
_destroyChooser: function() {
this.$().select2('destroy');
}.on('willDestroyElement')
});

View File

@ -28,6 +28,11 @@ export default Ember.Component.extend({
return !this.site.mobileView && this.currentUser && this.currentUser.get('canManageTopic');
},
showEditOnFooter: Ember.computed.and(
'topic.isPrivateMessage',
'site.can_tag_pms'
),
@computed('topic.message_archived')
archiveIcon: archived => archived ? '' : 'folder',

View File

@ -62,13 +62,14 @@ export default MountWidget.extend(Docking, {
this.dockBottom = false;
if (posTop < topicTop) {
this.dockAt = topicTop;
this.dockAt = parseInt(topicTop, 10);
} else if (pos > topicBottom + footerHeight) {
this.dockAt = (topicBottom - timelineHeight) + footerHeight;
this.dockAt = parseInt((topicBottom - timelineHeight) + footerHeight, 10);
this.dockBottom = true;
if (this.dockAt < 0) { this.dockAt = 0; }
} else {
this.dockAt = null;
this.fastDockAt = parseInt(topicBottom - timelineHeight + footerHeight - offsetTop, 10);
}
if (this.dockAt !== prev) {

View File

@ -140,7 +140,8 @@ export default Ember.Controller.extend({
return !this.site.mobileView &&
this.site.get('can_tag_topics') &&
canEditTitle &&
!creatingPrivateMessage;
!creatingPrivateMessage &&
(!this.get('model.topic.isPrivateMessage') || this.site.get('can_tag_pms'));
},
@computed('model.whisper', 'model.unlistTopic')

View File

@ -42,7 +42,7 @@ const controllerOpts = {
const tracker = this.topicTrackingState;
// Move inserted into topics
this.get('content').loadBefore(tracker.get('newIncoming'));
this.get('content').loadBefore(tracker.get('newIncoming'), true);
tracker.resetTracking();
return false;
},

View File

@ -29,45 +29,36 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
resetPassword() {
return this._submit('/session/forgot_password', 'forgot_password.complete');
},
if (this.get('submitDisabled')) return false;
this.set('disabled', true);
emailLogin() {
return this._submit('/u/email-login', 'email_login.complete');
this.clearFlash();
ajax('/session/forgot_password', {
data: { login: this.get('accountEmailOrUsername').trim() },
type: 'POST'
}).then(data => {
const accountEmailOrUsername = escapeExpression(this.get("accountEmailOrUsername"));
const isEmail = accountEmailOrUsername.match(/@/);
let key = `forgot_password.complete_${isEmail ? 'email' : 'username'}`;
if (data.user_found) {
this.set('offerHelp', I18n.t(`${key}_found`, {
email: accountEmailOrUsername,
username: accountEmailOrUsername
}));
} else {
this.flash(I18n.t(`${key}_not_found`, {
email: accountEmailOrUsername,
username: accountEmailOrUsername
}), 'error');
}
}).catch(e => {
this.flash(extractError(e), 'error');
}).finally(() => {
this.set('disabled', false);
});
return false;
}
},
_submit(route, translationKey) {
if (this.get('submitDisabled')) return false;
this.set('disabled', true);
ajax(route, {
data: { login: this.get('accountEmailOrUsername').trim() },
type: 'POST'
}).then(data => {
const escaped = escapeExpression(this.get('accountEmailOrUsername'));
const isEmail = this.get('accountEmailOrUsername').match(/@/);
let key = `${translationKey}_${isEmail ? 'email' : 'username'}`;
let extraClass;
if (data.user_found === true) {
key += '_found';
this.set('accountEmailOrUsername', '');
this.set('offerHelp', I18n.t(key, { email: escaped, username: escaped }));
} else {
if (data.user_found === false) {
key += '_not_found';
extraClass = 'error';
}
this.flash(I18n.t(key, { email: escaped, username: escaped }), extraClass);
}
}).catch(e => {
this.flash(extractError(e), 'error');
}).finally(() => {
this.set('disabled', false);
});
return false;
},
});

View File

@ -4,6 +4,9 @@ import showModal from 'discourse/lib/show-modal';
import { setting } from 'discourse/lib/computed';
import { findAll } from 'discourse/models/login-method';
import { escape } from 'pretty-text/sanitizer';
import { escapeExpression } from 'discourse/lib/utilities';
import { extractError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
// This is happening outside of the app via popup
const AuthErrors = [
@ -23,14 +26,23 @@ export default Ember.Controller.extend(ModalFunctionality, {
authenticate: null,
loggingIn: false,
loggedIn: false,
processingEmailLink: false,
showLoginButtons: true,
canLoginLocal: setting('enable_local_logins'),
canLoginLocalWithEmail: setting('enable_local_logins_via_email'),
loginRequired: Em.computed.alias('application.loginRequired'),
resetForm: function() {
this.set('authenticate', null);
this.set('loggingIn', false);
this.set('loggedIn', false);
this.setProperties({
'authenticate': null,
'loggingIn': false,
'loggedIn': false,
'secondFactorRequired': false,
'showLoginButtons': true,
});
$("#credentials").show();
$("#second-factor").hide();
},
// Determines whether at least one login button is enabled
@ -38,9 +50,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
return findAll(this.siteSettings).length > 0;
}.property(),
loginButtonText: function() {
return this.get('loggingIn') ? I18n.t('login.logging_in') : I18n.t('login.title');
}.property('loggingIn'),
@computed('loggingIn')
loginButtonLabel(loggingIn) {
return loggingIn ? 'login.logging_in' : 'login.title';
},
loginDisabled: Em.computed.or('loggingIn', 'loggedIn'),
@ -54,6 +67,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
return this.get('loggingIn') || this.get('authenticate');
}.property('loggingIn', 'authenticate'),
@computed('canLoginLocalWithEmail', 'loginName', 'processingEmailLink')
showLoginWithEmailLink(canLoginLocalWithEmail, loginName, processingEmailLink) {
return canLoginLocalWithEmail && !Ember.isEmpty(loginName) && !processingEmailLink;
},
actions: {
login() {
const self = this;
@ -67,13 +85,28 @@ export default Ember.Controller.extend(ModalFunctionality, {
this.set('loggingIn', true);
ajax("/session", {
data: { login: this.get('loginName'), password: this.get('loginPassword') },
type: 'POST'
type: 'POST',
data: {
login: this.get('loginName'),
password: this.get('loginPassword'),
second_factor_token: this.get('loginSecondFactor')
},
}).then(function (result) {
// Successful login
if (result && result.error) {
self.set('loggingIn', false);
if (result.reason === 'not_activated') {
if (result.reason === 'invalid_second_factor' && !self.get('secondFactorRequired')) {
$('#modal-alert').hide();
self.setProperties({
'secondFactorRequired': true,
'showLoginButtons': false,
});
$("#credentials").hide();
$("#second-factor").show();
return;
} else if (result.reason === 'not_activated') {
self.send('showNotActivated', {
username: self.get('loginName'),
sentTo: escape(result.sent_to_email),
@ -182,6 +215,37 @@ export default Ember.Controller.extend(ModalFunctionality, {
const forgotPasswordController = this.get('forgotPassword');
if (forgotPasswordController) { forgotPasswordController.set("accountEmailOrUsername", this.get("loginName")); }
this.send("showForgotPassword");
},
emailLogin() {
if (this.get('processingEmailLink')) {
return;
}
if (Ember.isEmpty(this.get('loginName'))){
this.flash(I18n.t('login.blank_username'), 'error');
return;
}
this.set('processingEmailLink', true);
ajax('/u/email-login', {
data: { login: this.get('loginName').trim() },
type: 'POST'
}).then(data => {
const loginName = escapeExpression(this.get('loginName'));
const isEmail = loginName.match(/@/);
let key = `email_login.complete_${isEmail ? 'email' : 'username'}`;
if (data.user_found) {
this.flash(I18n.t(`${key}_found`, { email: loginName, username: loginName }));
} else {
this.flash(I18n.t(`${key}_not_found`, { email: loginName, username: loginName }), 'error');
}
}).catch(e => {
this.flash(extractError(e), 'error');
}).finally(() => {
this.set('processingEmailLink', false);
});
}
},
@ -194,16 +258,28 @@ export default Ember.Controller.extend(ModalFunctionality, {
}).property('authenticate'),
authenticationComplete(options) {
const self = this;
function loginError(errorMsg, className) {
function loginError(errorMsg, className, callback) {
showModal('login');
Ember.run.next(function() {
Ember.run.next(() => {
callback();
self.flash(errorMsg, className || 'success');
self.set('authenticate', null);
});
}
if (options.omniauth_disallow_totp) {
return loginError(I18n.t('login.omniauth_disallow_totp'), 'error', () => {
this.setProperties({
'loginName': options.email,
'showLoginButtons': false,
});
$('#login-account-password').focus();
});
}
for (let i=0; i<AuthErrors.length; i++) {
const cond = AuthErrors[i];
if (options[cond]) {

View File

@ -8,6 +8,7 @@ import { userPath } from 'discourse/lib/url';
export default Ember.Controller.extend(PasswordValidation, {
isDeveloper: Ember.computed.alias('model.is_developer'),
admin: Ember.computed.alias('model.admin'),
secondFactorRequired: Ember.computed.alias('model.second_factor_required'),
passwordRequired: true,
errorMessage: null,
successMessage: null,
@ -32,7 +33,8 @@ export default Ember.Controller.extend(PasswordValidation, {
url: userPath(`password-reset/${this.get('model.token')}.json`),
type: 'PUT',
data: {
password: this.get('accountPassword')
password: this.get('accountPassword'),
second_factor_token: this.get('secondFactor')
}
}).then(result => {
if (result.success) {
@ -45,10 +47,22 @@ export default Ember.Controller.extend(PasswordValidation, {
DiscourseURL.redirectTo(result.redirect_to || '/');
}
} else {
if (result.errors && result.errors.password && result.errors.password.length > 0) {
if (result.errors && result.errors.user_second_factor) {
this.setProperties({
secondFactorRequired: true,
password: null,
errorMessage: result.message
});
} else if (this.get('secondFactorRequired')) {
this.setProperties({
secondFactorRequired: false,
errorMessage: null
});
} else if (result.errors && result.errors.password && result.errors.password.length > 0) {
this.get('rejectedPasswords').pushObject(this.get('accountPassword'));
this.get('rejectedPasswordsMessages').set(this.get('accountPassword'), result.errors.password[0]);
}
if (result.message) {
this.set('errorMessage', result.message);
}

View File

@ -40,6 +40,11 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController,
return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins;
},
@computed("model.second_factor_enabled")
secondFactorStatusClass(secondFactorEnabled) {
return secondFactorEnabled ? 'tip good' : 'tip bad';
},
actions: {
save() {
this.set('saved', false);

View File

@ -1,6 +1,6 @@
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { popupAjaxError } from 'discourse/lib/ajax-error';
import { default as computed } from "ember-addons/ember-computed-decorators";
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(PreferencesTabController, {
saveAttrNames: [
@ -12,7 +12,7 @@ export default Ember.Controller.extend(PreferencesTabController, {
@computed("model.watchedCategories", "model.watchedFirstPostCategories", "model.trackedCategories", "model.mutedCategories")
selectedCategories(watched, watchedFirst, tracked, muted) {
return [].concat(watched, watchedFirst, tracked, muted);
return [].concat(watched, watchedFirst, tracked, muted).filter(t => t);
},
canSave: function() {

View File

@ -0,0 +1,99 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
import { default as DiscourseURL, userPath } from 'discourse/lib/url';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import { LOGIN_METHODS } from 'discourse/models/login-method';
export default Ember.Controller.extend({
loading: false,
resetPasswordLoading: false,
resetPasswordProgress: '',
password: null,
secondFactorImage: null,
secondFactorKey: null,
showSecondFactorKey: false,
errorMessage: null,
newUsername: null,
loaded: Ember.computed.and('secondFactorImage', 'secondFactorKey'),
@computed('loading')
submitButtonText(loading) {
return loading ? 'loading' : 'submit';
},
@computed
displayOAuthWarning() {
return LOGIN_METHODS.some(name => {
return this.siteSettings[`enable_${name}_logins`];
});
},
toggleSecondFactor(enable) {
if (!this.get('secondFactorToken')) return;
this.set('loading', true);
this.get('content').toggleSecondFactor(this.get('secondFactorToken'), enable)
.then(response => {
if (response.error) {
this.set('errorMessage', response.error);
this.set('loading', false);
return;
}
this.set('errorMessage',null);
DiscourseURL.redirectTo(userPath(`${this.get('content').username.toLowerCase()}/preferences`));
})
.catch(error => {
this.set('loading', false);
popupAjaxError(error);
});
},
actions: {
confirmPassword() {
if (!this.get('password')) return;
this.set('loading', true);
this.get('content').loadSecondFactorCodes(this.get('password'))
.then(response => {
if(response.error) {
this.set('errorMessage', response.error);
return;
}
this.setProperties({
errorMessage: null,
secondFactorKey: response.key,
secondFactorImage: response.qr,
});
})
.catch(popupAjaxError)
.finally(() => this.set('loading', false));
},
resetPassword() {
this.setProperties({
resetPasswordLoading: true,
resetPasswordProgress: ''
});
return this.get('model').changePassword().then(() => {
this.set('resetPasswordProgress', I18n.t('user.change_password.success'));
})
.catch(popupAjaxError)
.finally(() => this.set('resetPasswordLoading', false));
},
showSecondFactorKey() {
this.set('showSecondFactorKey', true);
},
enableSecondFactor() {
this.toggleSecondFactor(true);
},
disableSecondFactor() {
this.toggleSecondFactor(false);
}
}
});

View File

@ -1,8 +1,8 @@
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(PreferencesTabController, {
saveAttrNames: [
'muted_tags',
'tracked_tags',
@ -10,6 +10,11 @@ export default Ember.Controller.extend(PreferencesTabController, {
'watching_first_post_tags'
],
@computed("model.watched_tags", "model.watching_first_post_tags", "model.tracked_tags", "model.muted_tags")
selectedTags(watched, watchedFirst, tracked, muted) {
return [].concat(watched, watchedFirst, tracked, muted).filter(t => t);
},
actions: {
save() {
this.set('saved', false);

View File

@ -104,7 +104,7 @@ export default Ember.Controller.extend(BufferedContent, {
@computed('model.isPrivateMessage')
canEditTags(isPrivateMessage) {
return !isPrivateMessage && this.site.get('can_tag_topics');
return this.site.get('can_tag_topics') && (!isPrivateMessage || this.site.get('can_tag_pms'));
},
actions: {
@ -265,6 +265,23 @@ export default Ember.Controller.extend(BufferedContent, {
}
},
editFirstPost() {
const postStream = this.get('model.postStream');
let firstPost = postStream.get('posts.firstObject');
if (firstPost.get('post_number') !== 1) {
const postId = postStream.findPostIdForPostNumber(1);
// try loading from identity map first
firstPost = postStream.findLoadedPost(postId);
if (firstPost === undefined) {
return this.get('model.postStream').loadPost(postId).then(post => {
this.send("editPost", post);
});
}
}
this.send("editPost", firstPost);
},
// Post related methods
replyToPost(post) {
const composerController = this.get('composer');

View File

@ -33,7 +33,6 @@ export default Ember.Controller.extend({
return hasSelection && pmView !== "archive" && !archive;
},
bulkOperation(operation) {
const selected = this.get('selected');
var params = {type: operation};

View File

@ -1,18 +1,59 @@
import computed from 'ember-addons/ember-computed-decorators';
// Lists of topics on a user's page.
export default Ember.Controller.extend({
application: Ember.inject.controller(),
hideCategory: false,
showPosters: false,
newIncoming: [],
incomingCount: 0,
channel: null,
_showFooter: function() {
this.set("application.showFooter", !this.get("model.canLoadMore"));
}.observes("model.canLoadMore"),
@computed('incomingCount')
hasIncoming(incomingCount) {
return incomingCount > 0;
},
subscribe(channel) {
this.set('channel', channel);
this.messageBus.subscribe(channel, data => {
if (this.get('newIncoming').indexOf(data.topic_id) === -1) {
this.get('newIncoming').push(data.topic_id);
this.incrementProperty('incomingCount');
}
});
},
unsubscribe() {
const channel = this.get('channel');
if (channel) this.messageBus.unsubscribe(channel);
this._resetTracking();
this.set('channel', null);
},
_resetTracking() {
this.setProperties({
"newIncoming": [],
"incomingCount": 0
});
},
actions: {
loadMore: function() {
this.get('model').loadMore();
}
},
showInserted() {
this.get('model').loadBefore(this.get('newIncoming'));
this._resetTracking();
return false;
},
},
});

View File

@ -36,7 +36,7 @@ function renderAvatar(user, options) {
if (!username || !avatarTemplate) { return ''; }
let formattedUsername = formatUsername(username);
let displayName = Ember.get(user, 'name') || formatUsername(username);
let title = options.title;
if (!title && !options.ignoreTitle) {
@ -49,7 +49,7 @@ function renderAvatar(user, options) {
// if a description has been provided
if (description && description.length > 0) {
// preprend the username before the description
title = formattedUsername + " - " + description;
title = displayName + " - " + description;
}
}
}
@ -57,7 +57,7 @@ function renderAvatar(user, options) {
return avatarImg({
size: options.imageSize,
extraClasses: Em.get(user, 'extras') || options.extraClasses,
title: title || formattedUsername,
title: title || displayName,
avatarTemplate: avatarTemplate
});
} else {

View File

@ -1,5 +1,7 @@
import logout from 'discourse/lib/logout';
let _showingLogout = false;
// Subscribe to "logout" change events via the Message Bus
export default {
name: "logout",
@ -7,14 +9,22 @@ export default {
initialize: function (container) {
const messageBus = container.lookup('message-bus:main');
const siteSettings = container.lookup('site-settings:main');
const keyValueStore = container.lookup('key-value-store:main');
if (!messageBus) { return; }
const callback = () => logout(siteSettings, keyValueStore);
messageBus.subscribe("/logout", function () {
bootbox.dialog(I18n.t("logout"), {label: I18n.t("refresh"), callback}, {onEscape: callback, backdrop: 'static'});
if (!_showingLogout) {
_showingLogout = true;
bootbox.dialog(I18n.t("logout"), {
label: I18n.t("refresh"),
callback: logout
}, {
onEscape: logout,
backdrop: 'static'
});
}
});
}
};

View File

@ -1,7 +1,9 @@
import pageVisible from 'discourse/lib/page-visible';
import logout from 'discourse/lib/logout';
let _trackView = false;
let _transientHeader = null;
let _showingLogout = false;
export function setTransientHeader(key, value) {
_transientHeader = {key, value};
@ -39,6 +41,10 @@ export function ajax() {
args.headers = args.headers || {};
if (Discourse.__container__.lookup('current-user:main')) {
args.headers['Discourse-Logged-In'] = "true";
}
if (_transientHeader) {
args.headers[_transientHeader.key] = _transientHeader.value;
_transientHeader = null;
@ -54,7 +60,22 @@ export function ajax() {
args.headers['Discourse-Visible'] = "true";
}
let handleLogoff = function(xhr) {
if (xhr.getResponseHeader('Discourse-Logged-Out') && !_showingLogout) {
_showingLogout = true;
bootbox.dialog(
I18n.t("logout"), {label: I18n.t("refresh"), callback: logout},
{
onEscape: () => logout(),
backdrop: 'static'
}
);
}
};
args.success = (data, textStatus, xhr) => {
handleLogoff(xhr);
if (xhr.getResponseHeader('Discourse-Readonly')) {
Ember.run(() => Discourse.Site.currentProp('isReadOnly', true));
}
@ -67,6 +88,8 @@ export function ajax() {
};
args.error = (xhr, textStatus, errorThrown) => {
handleLogoff(xhr);
// note: for bad CSRF we don't loop an extra request right away.
// this allows us to eliminate the possibility of having a loop.
if (xhr.status === 403 && xhr.responseText === "[\"BAD CSRF\"]") {

View File

@ -26,7 +26,7 @@ export default {
}
// don't track links in quotes or in elided part
let tracking = $link.parents('aside.quote,.elided').length === 0;
let tracking = $link.parents('aside.quote, .elided').length === 0;
let href = $link.attr('href') || $link.data('href');
@ -113,8 +113,10 @@ export default {
return false;
}
const isInternal = DiscourseURL.isInternal(href);
// If we're on the same site, use the router and track via AJAX
if (tracking && DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) {
if (tracking && isInternal && !$link.hasClass('attachment')) {
ajax("/clicks/track", {
data: {
url: href,
@ -128,9 +130,11 @@ export default {
return false;
}
// Otherwise, use a custom URL with a redirect
// consider CTRL+mouse-left-click / CMD+mouse-left-click or mouse-middle-click as well
if (Discourse.User.currentProp('external_links_in_new_tab') || ((e.ctrlKey || e.metaKey) && (e.which === 1)) || (e.which === 2)) {
const modifierLeftClicked = (e.ctrlKey || e.metaKey) && e.which === 1;
const middleClicked = e.which === 2;
const openExternalInNewTab = Discourse.User.currentProp('external_links_in_new_tab');
if (modifierLeftClicked || middleClicked || (!isInternal && openExternalInNewTab)) {
window.open(destUrl, '_blank').focus();
} else {
DiscourseURL.redirectTo(destUrl);

View File

@ -311,7 +311,7 @@ export function number(val) {
formattedNumber = I18n.toNumber(val / 1000000, {precision: 1});
return I18n.t("number.short.millions", {number: formattedNumber});
} else if (val > 99999) {
formattedNumber = I18n.toNumber(val / 1000, {precision: 0});
formattedNumber = I18n.toNumber(Math.floor(val / 1000), {precision: 0});
return I18n.t("number.short.thousands", {number: formattedNumber});
} else if (val > 999) {
formattedNumber = I18n.toNumber(val / 1000, {precision: 1});

View File

@ -1,4 +1,10 @@
export default function logout(siteSettings, keyValueStore) {
if (!siteSettings || !keyValueStore) {
const container = Discourse.__container__;
siteSettings = siteSettings || container.lookup('site-settings:main');
keyValueStore = keyValueStore || container.lookup('key-value-store:main');
}
keyValueStore.abandonLocal();
const redirect = siteSettings.logout_redirect;

View File

@ -3,7 +3,12 @@ export default function renderTag(tag, params) {
tag = Handlebars.Utils.escapeExpression(tag);
const classes = ['tag-' + tag, 'discourse-tag'];
const tagName = params.tagName || "a";
const href = (tagName === "a" && !params.noHref) ? " href='" + Discourse.getURL("/tags/" + tag) + "' " : "";
let path;
if (tagName === "a" && !params.noHref) {
const current_user = Discourse.User.current();
path = params.isPrivateMessage ? `/u/${current_user.username}/messages/tag/${tag}` : `/tags/${tag}`;
}
const href = path ? ` href='${Discourse.getURL(path)}' ` : "";
if (Discourse.SiteSettings.tag_style || params.style) {
classes.push(params.style || Discourse.SiteSettings.tag_style);

View File

@ -20,6 +20,7 @@ export function addTagsHtmlCallback(callback, options) {
export default function(topic, params){
let tags = topic.tags;
let buffer = "";
const isPrivateMessage = topic.get('isPrivateMessage');
if (params && params.mode === "list") {
tags = topic.get("visibleListTags");
@ -43,7 +44,7 @@ export default function(topic, params){
buffer = "<div class='discourse-tags'>";
if (tags) {
for(let i=0; i<tags.length; i++){
buffer += renderTag(tags[i]) + ' ';
buffer += renderTag(tags[i], { isPrivateMessage }) + ' ';
}
}

View File

@ -138,10 +138,12 @@ export default function transformPost(currentUser, site, post, prevPost, nextPos
postAtts.topicCreatedAt = topic.created_at;
postAtts.createdByUsername = createdBy.username;
postAtts.createdByAvatarTemplate = createdBy.avatar_template;
postAtts.createdByName = createdBy.name;
postAtts.lastPostUrl = topic.get('lastPostUrl');
postAtts.lastPostUsername = details.last_poster.username;
postAtts.lastPostAvatarTemplate = details.last_poster.avatar_template;
postAtts.lastPostName = details.last_poster.name;
postAtts.lastPostAt = topic.last_posted_at;
postAtts.topicReplyCount = topic.get('replyCount');

View File

@ -203,7 +203,7 @@ export function validateUploadedFile(file, opts) {
// check that the uploaded file is authorized
if (opts.allowStaffToUploadAnyFileInPm && opts.isPrivateMessage) {
if (Discourse.User.current("staff")) {
if (Discourse.User.currentProp('staff')) {
return true;
}
}
@ -239,16 +239,28 @@ export function validateUploadedFile(file, opts) {
const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)/i;
function extensionsToArray(exts) {
return exts.toLowerCase()
.replace(/[\s\.]+/g, "")
.split("|")
.filter(ext => ext.indexOf("*") === -1);
}
function extensions() {
return Discourse.SiteSettings.authorized_extensions
.toLowerCase()
.replace(/[\s\.]+/g, "")
.split("|")
.filter(ext => ext.indexOf("*") === -1);
return extensionsToArray(Discourse.SiteSettings.authorized_extensions);
}
function staffExtensions() {
return extensionsToArray(Discourse.SiteSettings.authorized_extensions_for_staff);
}
function imagesExtensions() {
return extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext));
let exts = extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext));
if (Discourse.User.currentProp('staff')) {
const staffExts = staffExtensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext));
exts = _.union(exts, staffExts);
}
return exts;
}
function extensionsRegex() {
@ -259,7 +271,14 @@ function imagesExtensionsRegex() {
return new RegExp("\\.(" + imagesExtensions().join("|") + ")$", "i");
}
function staffExtensionsRegex() {
return new RegExp("\\.(" + staffExtensions().join("|") + ")$", "i");
}
function isAuthorizedFile(fileName) {
if (Discourse.User.currentProp('staff') && staffExtensionsRegex().test(fileName)) {
return true;
}
return extensionsRegex().test(fileName);
}
@ -268,7 +287,8 @@ function isAuthorizedImage(fileName){
}
export function authorizedExtensions() {
return authorizesAllExtensions() ? "*" : extensions().join(", ");
const exts = Discourse.User.currentProp('staff') ? [...extensions(), ...staffExtensions()] : extensions();
return exts.filter(ext => ext.length > 0).join(", ");
}
export function authorizedImagesExtensions() {
@ -276,7 +296,9 @@ export function authorizedImagesExtensions() {
}
export function authorizesAllExtensions() {
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0;
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0 || (
Discourse.SiteSettings.authorized_extensions_for_staff.indexOf("*") >= 0 &&
Discourse.User.currentProp('staff'));
}
export function authorizesOneOrMoreExtensions() {
@ -322,7 +344,7 @@ export function allowsImages() {
}
export function allowsAttachments() {
return authorizesAllExtensions() || extensions().length > imagesExtensions().length;
return authorizesAllExtensions() || authorizedExtensions().split(", ").length > imagesExtensions().length;
}
export function uploadLocation(url) {

View File

@ -1,9 +1,9 @@
import { propertyEqual, setting } from 'discourse/lib/computed';
export default Ember.Mixin.create({
isOwnEmail: propertyEqual("model.id", "currentUser.id"),
isCurrentUser: propertyEqual("model.id", "currentUser.id"),
showEmailOnProfile: setting("show_email_on_profile"),
canStaffCheckEmails: Em.computed.and("showEmailOnProfile", "currentUser.staff"),
canAdminCheckEmails: Em.computed.alias("currentUser.admin"),
canCheckEmails: Em.computed.or("isOwnEmail", "canStaffCheckEmails", "canAdminCheckEmails"),
canCheckEmails: Em.computed.or("isCurrentUser", "canStaffCheckEmails", "canAdminCheckEmails"),
});

View File

@ -5,6 +5,10 @@ export default Ember.Mixin.create({
this.appEvents.trigger('modal-body:flash', { text, messageClass });
},
clearFlash() {
this.appEvents.trigger('modal-body:clearFlash');
},
showModal(...args) {
return showModal(...args);
},

View File

@ -97,7 +97,7 @@ const Category = RestModel.extend({
allow_badges: this.get('allow_badges'),
custom_fields: this.get('custom_fields'),
topic_template: this.get('topic_template'),
suppress_from_homepage: this.get('suppress_from_homepage'),
suppress_from_latest: this.get('suppress_from_latest'),
all_topics_wiki: this.get('all_topics_wiki'),
allowed_tags: this.get('allowed_tags'),
allowed_tag_groups: this.get('allowed_tag_groups'),

View File

@ -22,12 +22,21 @@ const LoginMethod = Ember.Object.extend({
let methods;
let preRegister;
export const LOGIN_METHODS = [
"google_oauth2",
"facebook",
"twitter",
"yahoo",
"instagram",
"github"
];
export function findAll(siteSettings, capabilities, isMobileDevice) {
if (methods) { return methods; }
methods = [];
[ "google_oauth2", "facebook", "cas", "twitter", "yahoo", "instagram", "github" ].forEach(name => {
LOGIN_METHODS.forEach(name => {
if (siteSettings["enable_" + name + "_logins"]) {
const params = { name };
if (name === "google_oauth2") {

View File

@ -70,17 +70,17 @@ const TopicList = RestModel.extend({
// loads topics with these ids "before" the current topics
loadBefore(topic_ids) {
loadBefore(topic_ids, storeInSession) {
const topicList = this,
topics = this.get('topics');
// refresh dupes
topics.removeObjects(topics.filter(topic => topic_ids.indexOf(topic.get('id')) >= 0));
const url = `${Discourse.getURL("/")}${this.get('filter')}?topic_ids=${topic_ids.join(",")}`;
const url = `${Discourse.getURL("/")}${this.get('filter')}.json?topic_ids=${topic_ids.join(",")}`;
const store = this.store;
return ajax({ url }).then(result => {
return ajax({ url, data: this.get("params") }).then(result => {
let i = 0;
topicList.forEachNew(TopicList.topicsFrom(store, result), function(t) {
// highlight the first of the new topics so we can get a visual feedback
@ -88,7 +88,7 @@ const TopicList = RestModel.extend({
topics.insertAt(i,t);
i++;
});
Discourse.Session.currentProp('topicList', topicList);
if (storeInSession) Discourse.Session.currentProp('topicList', topicList);
});
}
});

View File

@ -1,6 +1,5 @@
import { NotificationLevels } from 'discourse/lib/notification-levels';
import computed from "ember-addons/ember-computed-decorators";
import { on } from "ember-addons/ember-computed-decorators";
import { default as computed, on } from "ember-addons/ember-computed-decorators";
import { defaultHomepage } from 'discourse/lib/utilities';
import PreloadStore from 'preload-store';
@ -35,7 +34,7 @@ const TopicTrackingState = Discourse.Model.extend({
tracker.incrementMessageCount();
}
if (data.message_type === "new_topic" || data.message_type === "latest") {
if (["new_topic", "latest"].includes(data.message_type)) {
const muted_category_ids = Discourse.User.currentProp("muted_category_ids");
if (_.include(muted_category_ids, data.payload.category_id)) {
return;
@ -55,7 +54,7 @@ const TopicTrackingState = Discourse.Model.extend({
tracker.notify(data);
}
if (data.message_type === "new_topic" || data.message_type === "unread" || data.message_type === "read") {
if (["new_topic", "unread", "read"].includes(data.message_type)) {
tracker.notify(data);
const old = tracker.states["t" + data.topic_id];
@ -117,17 +116,17 @@ const TopicTrackingState = Discourse.Model.extend({
}
if (filter === defaultHomepage()) {
const suppressed_from_homepage_category_ids = Discourse.Site.currentProp("suppressed_from_homepage_category_ids");
if (_.include(suppressed_from_homepage_category_ids, data.payload.category_id)) {
const suppressed_from_latest_category_ids = Discourse.Site.currentProp("suppressed_from_latest_category_ids");
if (_.include(suppressed_from_latest_category_ids, data.payload.category_id)) {
return;
}
}
if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic") {
if (["all", "latest", "new"].includes(filter) && data.message_type === "new_topic") {
this.addIncoming(data.topic_id);
}
if ((filter === "all" || filter === "unread") && data.message_type === "unread") {
if (["all", "unread"].includes(filter) && data.message_type === "unread") {
const old = this.states["t" + data.topic_id];
if(!old || old.highest_post_number === old.last_read_post_number) {
this.addIncoming(data.topic_id);

View File

@ -23,14 +23,16 @@ const User = RestModel.extend({
hasPMs: Em.computed.gt("private_messages_stats.all", 0),
hasStartedPMs: Em.computed.gt("private_messages_stats.mine", 0),
hasUnreadPMs: Em.computed.gt("private_messages_stats.unread", 0),
hasPosted: Em.computed.gt("post_count", 0),
hasNotPosted: Em.computed.not("hasPosted"),
canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"),
redirected_to_top: {
reason: null,
},
@computed("can_be_deleted", "post_count")
canBeDeleted(canBeDeleted, postCount) {
return canBeDeleted && postCount <= 5;
},
@computed()
stream() {
return UserStream.create({ user: this });
@ -304,6 +306,20 @@ const User = RestModel.extend({
});
},
loadSecondFactorCodes(password) {
return ajax("/u/second_factors.json", {
data: { password },
type: 'POST'
});
},
toggleSecondFactor(token, enable) {
return ajax("/u/second_factor.json", {
data: { second_factor_token: token, enable },
type: 'PUT'
});
},
loadUserAction(id) {
const stream = this.get('stream');
return ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => {

View File

@ -96,6 +96,7 @@ export default function() {
this.route('archive');
this.route('group', { path: 'group/:name'});
this.route('groupArchive', { path: 'group/:name/archive'});
this.route('tag', { path: 'tag/:id'});
});
this.route('preferences', { resetNamespace: true }, function() {
@ -110,6 +111,7 @@ export default function() {
this.route('username');
this.route('email');
this.route('second-factor');
this.route('about', { path: '/about-me' });
this.route('badgeTitle', { path: '/badge_title' });
this.route('card-badge', { path: '/card-badge' });

View File

@ -1,3 +1,5 @@
import { emojiUnescape } from 'discourse/lib/text';
export default function (filter) {
return Discourse.Route.extend({
actions: {
@ -20,6 +22,12 @@ export default function (filter) {
// initialize "canLoadMore"
model.set("canLoadMore", model.get("itemsLoaded") === 60);
model.get('content').forEach((item) => {
if (item.get('title')) {
item.set('title', emojiUnescape(Handlebars.Utils.escapeExpression(item.title)));
}
});
this.controllerFor("user-posts").set("model", model);
},

View File

@ -1,10 +1,15 @@
import UserTopicListRoute from "discourse/routes/user-topic-list";
// A helper to build a user topic list route
export default (viewName, path) => {
export default (viewName, path, channel) => {
return UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.messages_received,
titleToken() {
const key = viewName === "index" ? "inbox" : viewName;
return [I18n.t(`user.messages.${key}`), I18n.t("user.private_messages")];
},
actions: {
didTransition() {
this.controllerFor("user-topics-list")._showFooter();
@ -19,6 +24,10 @@ export default (viewName, path) => {
setupController() {
this._super.apply(this, arguments);
if (channel) {
this.controllerFor("user-topics-list").subscribe(`/private-messages/${channel}`);
}
this.controllerFor("user-topics-list").setProperties({
hideCategory: true,
showPosters: true,
@ -32,6 +41,8 @@ export default (viewName, path) => {
},
deactivate() {
this.controllerFor('user-topics-list').unsubscribe();
this.searchService.set(
'searchContext',
this.controllerFor("user").get("model.searchContext")

View File

@ -91,7 +91,7 @@ export default function(filter, extras) {
const topicOpts = {
model,
category: null,
period: model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''),
period: model.get('for_period') || (filter.indexOf('top/') >= 0 ? filter.split('/')[1] : ''),
selected: [],
expandGloballyPinned: true
};

View File

@ -12,20 +12,24 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
this.render("discovery/categories", { outlet: "list-container" });
},
model() {
const style = !this.site.mobileView && this.siteSettings.desktop_category_page_style;
const parentCategory = this.get("model.parentCategory");
findCategories() {
let style = !this.site.mobileView &&
this.siteSettings.desktop_category_page_style;
let promise;
let parentCategory = this.get("model.parentCategory");
if (parentCategory) {
promise = CategoryList.listForParent(this.store, parentCategory);
return CategoryList.listForParent(this.store, parentCategory);
} else if (style === "categories_and_latest_topics") {
promise = this._loadCategoriesAndLatestTopics();
} else {
promise = CategoryList.list(this.store);
return this._findCategoriesAndTopics('latest');
} else if (style === "categories_and_top_topics") {
return this._findCategoriesAndTopics('top');
}
return promise.then(model => {
return CategoryList.list(this.store);
},
model() {
return this.findCategories().then(model => {
const tracking = this.topicTrackingState;
if (tracking) {
tracking.sync(model, "categories");
@ -35,26 +39,31 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
});
},
_loadCategoriesAndLatestTopics() {
const wrappedCategoriesList = PreloadStore.getAndRemove("categories_list");
const topicListLatest = PreloadStore.getAndRemove("topic_list_latest");
const categoriesList = wrappedCategoriesList && wrappedCategoriesList.category_list;
if (categoriesList && topicListLatest) {
return new Ember.RSVP.Promise(resolve => {
const result = Ember.Object.create({
categories: CategoryList.categoriesFrom(this.store, wrappedCategoriesList),
topics: TopicList.topicsFrom(this.store, topicListLatest),
_findCategoriesAndTopics(filter) {
return Ember.RSVP.hash({
wrappedCategoriesList: PreloadStore.getAndRemove("categories_list"),
topicsList: PreloadStore.getAndRemove(`topic_list_${filter}`)
}).then(hash => {
let { wrappedCategoriesList, topicsList } = hash;
let categoriesList = wrappedCategoriesList &&
wrappedCategoriesList.category_list;
if (categoriesList && topicsList) {
return Ember.Object.create({
categories: CategoryList.categoriesFrom(
this.store,
wrappedCategoriesList
),
topics: TopicList.topicsFrom(this.store, topicsList),
can_create_category: categoriesList.can_create_category,
can_create_topic: categoriesList.can_create_topic,
draft_key: categoriesList.draft_key,
draft: categoriesList.draft,
draft_sequence: categoriesList.draft_sequence
});
resolve(result);
});
} else {
return ajax("/categories_and_latest").then(result => {
}
// Otherwise, return the ajax result
return ajax(`/categories_and_${filter}`).then(result => {
return Ember.Object.create({
categories: CategoryList.categoriesFrom(this.store, result),
topics: TopicList.topicsFrom(this.store, result),
@ -65,7 +74,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
draft_sequence: result.category_list.draft_sequence
});
});
}
});
},
titleToken() {

View File

@ -0,0 +1,15 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
model() {
return this.modelFor('user');
},
renderTemplate() {
return this.render({ into: 'user' });
},
setupController(controller, model) {
controller.setProperties({ model, newUsername: model.get('username') });
}
});

View File

@ -15,6 +15,10 @@ export default RestrictedUserRoute.extend({
},
actions: {
showTwoFactorModal() {
showModal('second-factor-intro');
},
showAvatarSelector() {
showModal('avatar-selector');

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('archive', 'private-messages-archive');
export default createPMRoute('archive', 'private-messages-archive', 'archive');

View File

@ -1,26 +1,41 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('groups', 'private-messages-groups').extend({
model(params) {
const username = this.modelFor("user").get("username_lower");
return this.store.findFiltered("topicList", {
filter: `topics/private-messages-group/${username}/${params.name}/archive`
});
},
groupName: null,
afterModel(model) {
const split = model.get("filter").split('/');
const groupName = split[split.length-2];
const groups = this.modelFor("user").get("groups");
const group = _.first(groups.filterBy("name", groupName));
this.controllerFor("user-private-messages").set("group", group);
},
titleToken() {
const groupName = this.get('groupName');
setupController(controller, model) {
this._super.apply(this, arguments);
const split = model.get("filter").split('/');
const group = split[split.length-2];
this.controllerFor("user-private-messages").set("groupFilter", group);
this.controllerFor("user-private-messages").set("archive", true);
}
if (groupName) {
return [
`${groupName.capitalize()} ${I18n.t('user.messages.archive')}`,
I18n.t("user.private_messages")
];
};
},
model(params) {
const username = this.modelFor("user").get("username_lower");
return this.store.findFiltered("topicList", {
filter: `topics/private-messages-group/${username}/${params.name}/archive`
});
},
afterModel(model) {
const split = model.get("filter").split('/');
const groupName = split[split.length-2];
this.set("groupName", groupName);
const groups = this.modelFor("user").get("groups");
const group = _.first(groups.filterBy("name", groupName));
this.controllerFor("user-private-messages").set("group", group);
},
setupController(controller, model) {
this._super.apply(this, arguments);
const split = model.get("filter").split('/');
const group = split[split.length-2];
this.controllerFor("user-private-messages").set("groupFilter", group);
this.controllerFor("user-private-messages").set("archive", true);
this.controllerFor("user-topics-list").subscribe(`/private-messages/group/${group}/archive`);
}
});

View File

@ -1,24 +1,33 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('groups', 'private-messages-groups').extend({
model(params) {
const username = this.modelFor("user").get("username_lower");
return this.store.findFiltered("topicList", {
filter: `topics/private-messages-group/${username}/${params.name}`
});
},
groupName: null,
afterModel(model) {
const groupName = _.last(model.get("filter").split('/'));
const groups = this.modelFor("user").get("groups");
const group = _.first(groups.filterBy("name", groupName));
this.controllerFor("user-private-messages").set("group", group);
},
titleToken() {
const groupName = this.get('groupName');
if (groupName) return [groupName.capitalize(), I18n.t("user.private_messages")];
},
setupController(controller, model) {
this._super.apply(this, arguments);
const group = _.last(model.get("filter").split('/'));
this.controllerFor("user-private-messages").set("groupFilter", group);
this.controllerFor("user-private-messages").set("archive", false);
}
model(params) {
const username = this.modelFor("user").get("username_lower");
return this.store.findFiltered("topicList", {
filter: `topics/private-messages-group/${username}/${params.name}`
});
},
afterModel(model) {
const groupName = _.last(model.get("filter").split('/'));
this.set("groupName", groupName);
const groups = this.modelFor("user").get("groups");
const group = _.first(groups.filterBy("name", groupName));
this.controllerFor("user-private-messages").set("group", group);
},
setupController(controller, model) {
this._super.apply(this, arguments);
const group = _.last(model.get("filter").split('/'));
this.controllerFor("user-private-messages").set("groupFilter", group);
this.controllerFor("user-private-messages").set("archive", false);
this.controllerFor("user-topics-list").subscribe(`/private-messages/group/${group}`);
}
});

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('index', 'private-messages');
export default createPMRoute('index', 'private-messages', 'inbox');

View File

@ -1,3 +1,3 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('sent', 'private-messages-sent');
export default createPMRoute('sent', 'private-messages-sent', 'sent');

View File

@ -0,0 +1,10 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('tags', 'private-messages-tags').extend({
model(params) {
const username = this.modelFor("user").get("username_lower");
return this.store.findFiltered("topicList", {
filter: `topics/private-messages-tag/${username}/${params.id}`
});
}
});

View File

@ -8,10 +8,11 @@
<div class='title'>
<h3>{{bg.badgeGrouping.displayName}}</h3>
</div>
{{#each bg.badges as |b|}}
{{badge-card badge=b filterUser=b.has_badge username=currentUser.username}}
{{/each}}
<div class="badge-group-list">
{{#each bg.badges as |b|}}
{{badge-card badge=b filterUser=b.has_badge username=currentUser.username}}
{{/each}}
</div>
</div>
{{/each}}
</div>

View File

@ -37,6 +37,7 @@
{{#if userBadges}}
<div class="user-badges {{model.slug}}">
{{#load-more selector=".badge-info" action="loadMore"}}
<div class="badges-granted">
{{#each userBadges as |ub|}}
{{#user-info user=ub.user size="medium" class="badge-info" date=ub.granted_at}}
<div class="granted-on">{{i18n 'badges.granted_on' date=(inline-date ub.granted_at)}}</div>
@ -45,6 +46,7 @@
{{/if}}
{{/user-info}}
{{/each}}
</div>
{{/load-more}}
{{#unless canLoadMore}}

View File

@ -1,5 +1,5 @@
<p>{{i18n (concat "topics.bulk." title)}}</p>
<p>{{tag-chooser tags=tags categoryId=categoryId}}</p>
<p>{{tag-chooser filterPlaceholder=null tags=tags categoryId=categoryId}}</p>
{{d-button action=action disabled=emptyTags label=(concat "topics.bulk." label)}}

View File

@ -5,13 +5,15 @@
<span class='check-display status-checked'>{{d-icon "check"}}</span>
{{/if}}
<div class='badge-contents'>
<div class='badge-icon {{badge.badgeTypeClassName}}'>
{{icon-or-image badge.icon}}
</div>
<div class='badge-info'>
<div class='badge-info-item'>
<h3><a href={{url}}>{{badge.name}}</a></h3>
<div class='badge-summary'>{{{summary}}}</div>
<a href={{url}} class="badge-link">
<div class='badge-icon {{badge.badgeTypeClassName}}'>
{{icon-or-image badge.icon}}
</div>
</div>
<div class='badge-info'>
<div class='badge-info-item'>
<h3>{{badge.name}}</h3>
<div class='badge-summary'>{{{summary}}}</div>
</div>
</div>
</a>
</div>

View File

@ -1,4 +1,13 @@
{{#conditional-loading-spinner condition=loading}}
{{#if hasIncoming}}
<div class="show-mores">
<div class='alert alert-info clickable' {{action "showInserted"}}>
{{count-i18n key="topic_count_" suffix="latest" count=incomingCount}}
{{i18n 'click_to_show'}}
</div>
</div>
{{/if}}
{{#if topics}}
{{topic-list showParticipants=showParticipants
showPosters=showPosters

View File

@ -3,5 +3,5 @@
</div>
<div class='column'>
{{latest-topic-list topics=topics}}
{{categories-topic-list topics=topics filter="latest" class="latest-topic-list"}}
</div>

View File

@ -0,0 +1,7 @@
<div class='column categories'>
{{categories-only categories=categories}}
</div>
<div class='column'>
{{categories-topic-list topics=topics filter="top" class="top-topic-list"}}
</div>

View File

@ -11,6 +11,7 @@
{{#if c.read_restricted}}
{{d-icon 'lock'}}
{{/if}}
{{category-title-before category=c}}
{{c.name}}
</h3>
</a>

View File

@ -24,6 +24,7 @@
<div class='subcategories'>
{{#each c.subcategories as |s|}}
<span class='subcategory'>
{{category-title-before category=s}}
{{category-link s hideParent="true"}}
{{category-unread category=s}}
</span>

View File

@ -0,0 +1,16 @@
<div class='table-heading' aria-role="heading" aria-level="2">
{{i18n (concat "filters." filter ".title")}}
</div>
{{#if topics}}
{{#each topics as |t|}}
{{latest-topic-list-item topic=t}}
{{/each}}
<div class="more-topics">
<a href="/{{filter}}" class="btn pull-right">{{i18n "more"}}</a>
</div>
{{else}}
<div class='no-topics'>
<h3>{{i18n (concat "topics.none." filter)}}</h3>
</div>
{{/if}}

View File

@ -0,0 +1 @@
{{plugin-outlet name="category-title-before" noTags=true args=(hash category=category)}}

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