Version bump
This commit is contained in:
commit
0c40e2dddf
12
.eslintrc
12
.eslintrc
@ -42,12 +42,12 @@
|
||||
"invisible":true,
|
||||
"asyncRender":true,
|
||||
"selectDropdown":true,
|
||||
"selectBox":true,
|
||||
"expandSelectBox":true,
|
||||
"collapseSelectBox":true,
|
||||
"selectBoxSelectRow":true,
|
||||
"selectBoxSelectNoneRow":true,
|
||||
"selectBoxFillInFilter":true,
|
||||
"selectKit":true,
|
||||
"expandSelectKit":true,
|
||||
"collapseSelectKit":true,
|
||||
"selectKitSelectRow":true,
|
||||
"selectKitSelectNoneRow":true,
|
||||
"selectKitFillInFilter":true,
|
||||
"asyncTestDiscourse":true,
|
||||
"fixture":true,
|
||||
"find":true,
|
||||
|
||||
23
.overcommit.yml
Normal file
23
.overcommit.yml
Normal file
@ -0,0 +1,23 @@
|
||||
# Use this file to configure the Overcommit hooks you wish to use. This will
|
||||
# extend the default configuration defined in:
|
||||
# https://github.com/brigade/overcommit/blob/master/config/default.yml
|
||||
#
|
||||
# At the topmost level of this YAML file is a key representing type of hook
|
||||
# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
|
||||
# customize each hook, such as whether to only run it on certain files (via
|
||||
# `include`), whether to only display output if it fails (via `quiet`), etc.
|
||||
#
|
||||
# For a complete list of hooks, see:
|
||||
# https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook
|
||||
#
|
||||
# For a complete list of options that you can use to customize hooks, see:
|
||||
# https://github.com/brigade/overcommit#configuration
|
||||
|
||||
PreCommit:
|
||||
RuboCop:
|
||||
enabled: true
|
||||
command: ['bundle', 'exec', 'rubocop']
|
||||
EsLint:
|
||||
enabled: true
|
||||
command: ['eslint', '--ext', '.es6', '-f', 'compact']
|
||||
include: '**/*.es6'
|
||||
@ -6,6 +6,7 @@ AllCops:
|
||||
- 'bundle/**/*'
|
||||
- 'vendor/**/*'
|
||||
- 'node_modules/**/*'
|
||||
- 'public/**/*'
|
||||
|
||||
# Prefer &&/|| over and/or.
|
||||
Style/AndOr:
|
||||
|
||||
8
Gemfile
8
Gemfile
@ -22,12 +22,13 @@ else
|
||||
gem 'activesupport', '~> 5.1'
|
||||
gem 'railties', '~> 5.1'
|
||||
gem 'sprockets-rails'
|
||||
gem 'seed-fu', '~> 2.3.5'
|
||||
gem 'seed-fu'
|
||||
end
|
||||
|
||||
gem 'mail'
|
||||
gem 'mime-types', require: 'mime/types/columnar'
|
||||
gem 'mini_mime'
|
||||
gem 'mini_suffix'
|
||||
|
||||
gem 'hiredis'
|
||||
gem 'redis', require: ["redis", "redis/connection/hiredis"]
|
||||
@ -35,7 +36,7 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.8.19'
|
||||
gem 'onebox', '1.8.28'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
@ -46,7 +47,7 @@ gem 'barber'
|
||||
|
||||
gem 'message_bus'
|
||||
|
||||
gem 'rails_multisite', '~> 1.1.0.rc4'
|
||||
gem 'rails_multisite'
|
||||
|
||||
gem 'fast_xs'
|
||||
|
||||
@ -173,6 +174,7 @@ gem 'memory_profiler', require: false, platform: :mri
|
||||
gem 'cppjieba_rb', require: false
|
||||
|
||||
gem 'lograge', require: false
|
||||
gem 'logstash-event', require: false
|
||||
gem 'logstash-logger', require: false
|
||||
gem 'logster'
|
||||
|
||||
|
||||
26
Gemfile.lock
26
Gemfile.lock
@ -166,7 +166,7 @@ GEM
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
memory_profiler (0.9.8)
|
||||
message_bus (2.0.8)
|
||||
message_bus (2.0.9)
|
||||
rack (>= 1.1.3)
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
@ -177,6 +177,8 @@ GEM
|
||||
mini_portile2 (2.3.0)
|
||||
mini_racer (0.1.11)
|
||||
libv8 (~> 5.7)
|
||||
mini_suffix (0.3.0)
|
||||
ffi (~> 1.9)
|
||||
minitest (5.10.3)
|
||||
mocha (1.2.1)
|
||||
metaclass (~> 0.0.1)
|
||||
@ -230,7 +232,7 @@ GEM
|
||||
omniauth-twitter (1.3.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.19)
|
||||
onebox (1.8.28)
|
||||
fast_blank (>= 1.0.0)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
@ -259,7 +261,7 @@ GEM
|
||||
puma (3.9.1)
|
||||
r2 (0.2.6)
|
||||
rack (2.0.3)
|
||||
rack-mini-profiler (0.10.5)
|
||||
rack-mini-profiler (0.10.7)
|
||||
rack (>= 1.2.0)
|
||||
rack-openid (1.3.1)
|
||||
rack (>= 1.1.0)
|
||||
@ -273,7 +275,7 @@ GEM
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails_multisite (1.1.0.rc4)
|
||||
rails_multisite (1.1.2)
|
||||
activerecord (> 4.2, < 6)
|
||||
railties (> 4.2, < 6)
|
||||
railties (5.1.4)
|
||||
@ -284,7 +286,7 @@ GEM
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.2.2)
|
||||
rake
|
||||
raindrops (0.18.0)
|
||||
raindrops (0.19.0)
|
||||
rake (12.1.0)
|
||||
rake-compiler (1.0.4)
|
||||
rake
|
||||
@ -352,7 +354,7 @@ GEM
|
||||
bundler
|
||||
ffi (~> 1.9.6)
|
||||
sass (>= 3.3.0)
|
||||
seed-fu (2.3.6)
|
||||
seed-fu (2.3.7)
|
||||
activerecord (>= 3.1)
|
||||
activesupport (>= 3.1)
|
||||
shoulda (3.5.0)
|
||||
@ -388,7 +390,7 @@ GEM
|
||||
unf_ext
|
||||
unf_ext (0.0.7.4)
|
||||
unicode-display_width (1.3.0)
|
||||
unicorn (5.3.0)
|
||||
unicorn (5.3.1)
|
||||
kgio (~> 2.6)
|
||||
raindrops (~> 0.7)
|
||||
uniform_notifier (1.10.0)
|
||||
@ -441,6 +443,7 @@ DEPENDENCIES
|
||||
http_accept_language (~> 2.0.5)
|
||||
listen
|
||||
lograge
|
||||
logstash-event
|
||||
logstash-logger
|
||||
logster
|
||||
lru_redux
|
||||
@ -450,6 +453,7 @@ DEPENDENCIES
|
||||
mime-types
|
||||
mini_mime
|
||||
mini_racer
|
||||
mini_suffix
|
||||
minitest
|
||||
mocha
|
||||
mock_redis
|
||||
@ -466,7 +470,7 @@ DEPENDENCIES
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.8.19)
|
||||
onebox (= 1.8.28)
|
||||
openid-redis-store
|
||||
pg
|
||||
pry-nav
|
||||
@ -475,7 +479,7 @@ DEPENDENCIES
|
||||
r2 (~> 0.2.5)
|
||||
rack-mini-profiler
|
||||
rack-protection
|
||||
rails_multisite (~> 1.1.0.rc4)
|
||||
rails_multisite
|
||||
railties (~> 5.1)
|
||||
rake
|
||||
rb-fsevent
|
||||
@ -493,7 +497,7 @@ DEPENDENCIES
|
||||
ruby-readability
|
||||
sanitize
|
||||
sassc
|
||||
seed-fu (~> 2.3.5)
|
||||
seed-fu
|
||||
shoulda
|
||||
sidekiq
|
||||
simple-rss
|
||||
@ -507,4 +511,4 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
BUNDLED WITH
|
||||
1.15.4
|
||||
1.16.0
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'div',
|
||||
|
||||
_init: function(){
|
||||
this.$("input").select2({
|
||||
multiple: true,
|
||||
width: '100%',
|
||||
query: function(opts) {
|
||||
opts.callback({
|
||||
results: this.get("available").filter(function(o) {
|
||||
return -1 !== o.name.toLowerCase().indexOf(opts.term.toLowerCase());
|
||||
}).map(this._format)
|
||||
});
|
||||
}.bind(this)
|
||||
}).on("change", function(evt) {
|
||||
if (evt.added){
|
||||
this.triggerAction({
|
||||
action: "groupAdded",
|
||||
actionContext: this.get("available").findBy("id", evt.added.id)
|
||||
});
|
||||
} else if (evt.removed) {
|
||||
this.triggerAction({
|
||||
action:"groupRemoved",
|
||||
actionContext: evt.removed.id
|
||||
});
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this._refreshOnReset();
|
||||
}.on("didInsertElement"),
|
||||
|
||||
_format(item) {
|
||||
return {
|
||||
"text": item.name,
|
||||
"id": item.id,
|
||||
"locked": item.automatic
|
||||
};
|
||||
},
|
||||
|
||||
_refreshOnReset: function() {
|
||||
this.$("input").select2("data", this.get("selected").map(this._format));
|
||||
}.observes("selected")
|
||||
});
|
||||
3
app/assets/javascripts/admin/components/admin-nav.js.es6
Normal file
3
app/assets/javascripts/admin/components/admin-nav.js.es6
Normal file
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
tagName: ''
|
||||
});
|
||||
@ -32,12 +32,8 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
showAgreeFlagModal() {
|
||||
this._spawnModal('admin-agree-flag', this.get('flaggedPost'), 'agree-flag-modal');
|
||||
},
|
||||
|
||||
showDeleteFlagModal() {
|
||||
this._spawnModal('admin-delete-flag', this.get('flaggedPost'), 'delete-flag-modal');
|
||||
removeAfter(promise) {
|
||||
this.removeAfter(promise);
|
||||
},
|
||||
|
||||
disagree() {
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
/**
|
||||
Provide a nice GUI for a pipe-delimited list in the site settings.
|
||||
|
||||
@param settingValue is a reference to SiteSetting.value.
|
||||
@param choices is a reference to SiteSetting.choices
|
||||
**/
|
||||
export default Ember.Component.extend({
|
||||
|
||||
_select2FormatSelection: function(selectedObject, jqueryWrapper, htmlEscaper) {
|
||||
var text = selectedObject.text;
|
||||
if (text.length <= 6) {
|
||||
jqueryWrapper.closest('li.select2-search-choice').css({"border-bottom": '7px solid #'+text});
|
||||
}
|
||||
return htmlEscaper(text);
|
||||
},
|
||||
|
||||
_initializeSelect2: function(){
|
||||
var options = {
|
||||
multiple: false,
|
||||
separator: "|",
|
||||
tokenSeparators: ["|"],
|
||||
tags : this.get("choices") || [],
|
||||
width: 'off',
|
||||
dropdownCss: this.get("choices") ? {} : {display: 'none'},
|
||||
selectOnBlur: this.get("choices") ? false : true
|
||||
};
|
||||
|
||||
var settingName = this.get('settingName');
|
||||
if (typeof settingName === 'string' && settingName.indexOf('colors') > -1) {
|
||||
options.formatSelection = this._select2FormatSelection;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.$("input").select2(options).on("change", function(obj) {
|
||||
self.set("settingValue", obj.val.join("|"));
|
||||
self.refreshSortables();
|
||||
});
|
||||
|
||||
this.refreshSortables();
|
||||
}.on('didInsertElement'),
|
||||
|
||||
refreshOnReset: function() {
|
||||
this.$("input").select2("val", this.get("settingValue").split("|"));
|
||||
}.observes("settingValue"),
|
||||
|
||||
refreshSortables: function() {
|
||||
var self = this;
|
||||
this.$("ul.select2-choices").sortable().on('sortupdate', function() {
|
||||
self.$("input").select2("onSortEnd");
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -7,7 +7,7 @@ import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const PROBLEMS_CHECK_MINUTES = 1;
|
||||
|
||||
const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'blocked', 'suspended', 'top_traffic_sources',
|
||||
const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'silenced', 'suspended', 'top_traffic_sources',
|
||||
'top_referred_topics', 'updated_at'];
|
||||
|
||||
const REPORTS = [ 'global_reports', 'page_view_reports', 'private_message_reports', 'http_reports',
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export default Ember.Controller.extend({
|
||||
loading: false,
|
||||
period: "all"
|
||||
});
|
||||
@ -30,7 +30,7 @@ export default Ember.Controller.extend({
|
||||
|
||||
showInstructions: Ember.computed.gt('model.length', 0),
|
||||
|
||||
refresh: function() {
|
||||
_refresh() {
|
||||
this.set('loading', true);
|
||||
|
||||
var filters = this.get('filters'),
|
||||
@ -65,14 +65,18 @@ export default Ember.Controller.extend({
|
||||
});
|
||||
},
|
||||
|
||||
scheduleRefresh() {
|
||||
Ember.run.scheduleOnce('afterRender', this, this._refresh);
|
||||
},
|
||||
|
||||
resetFilters: function() {
|
||||
this.set('filters', Ember.Object.create());
|
||||
this.refresh();
|
||||
this.scheduleRefresh();
|
||||
}.on('init'),
|
||||
|
||||
_changeFilters: function(props) {
|
||||
this.get('filters').setProperties(props);
|
||||
this.refresh();
|
||||
this.scheduleRefresh();
|
||||
},
|
||||
|
||||
actions: {
|
||||
@ -91,7 +95,7 @@ export default Ember.Controller.extend({
|
||||
this._changeFilters(changed);
|
||||
},
|
||||
|
||||
clearAllFilters: function() {
|
||||
clearAllFilters() {
|
||||
this.set("filterActionId", null);
|
||||
this.resetFilters();
|
||||
},
|
||||
|
||||
@ -58,8 +58,8 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
saveTrustLevel() { return this.get("model").saveTrustLevel(); },
|
||||
restoreTrustLevel() { return this.get("model").restoreTrustLevel(); },
|
||||
lockTrustLevel(locked) { return this.get("model").lockTrustLevel(locked); },
|
||||
unblock() { return this.get("model").unblock(); },
|
||||
block() { return this.get("model").block(); },
|
||||
unsilence() { return this.get("model").unsilence(); },
|
||||
silence() { return this.get("model").silence(); },
|
||||
deleteAllPosts() { return this.get("model").deleteAllPosts(); },
|
||||
anonymize() { return this.get('model').anonymize(); },
|
||||
destroy() { return this.get('model').destroy(); },
|
||||
@ -70,7 +70,9 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
unsuspend() {
|
||||
this.get("model").unsuspend().catch(popupAjaxError);
|
||||
},
|
||||
|
||||
showSilenceModal() {
|
||||
this.get('adminTools').showSilenceModal(this.get('model'));
|
||||
},
|
||||
|
||||
toggleUsernameEdit() {
|
||||
this.set('userUsernameValue', this.get('model.username'));
|
||||
|
||||
@ -1,5 +1,20 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
showBadges: function() {
|
||||
return this.get('currentUser.admin') && this.siteSettings.enable_badges;
|
||||
}.property()
|
||||
application: Ember.inject.controller(),
|
||||
|
||||
@computed
|
||||
showBadges() {
|
||||
return this.currentUser.get('admin') && this.siteSettings.enable_badges;
|
||||
},
|
||||
|
||||
@computed('application.currentPath')
|
||||
adminContentsClassName(currentPath) {
|
||||
return currentPath.split('.').filter(segment => {
|
||||
return segment !== 'index' &&
|
||||
segment !== 'loading' &&
|
||||
segment !== 'show' &&
|
||||
segment !== 'admin';
|
||||
}).map(Ember.String.dasherize).join(' ');
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import DeleteSpammerModal from 'admin/mixins/delete-spammer-modal';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, DeleteSpammerModal, {
|
||||
removeAfter: null,
|
||||
|
||||
actions: {
|
||||
agreeDeleteSpammer(user) {
|
||||
return this.removeAfter(user.deleteAsSpammer()).then(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
},
|
||||
|
||||
perform(action) {
|
||||
let flaggedPost = this.get('model');
|
||||
return this.removeAfter(flaggedPost.agreeFlags(action)).then(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
@ -1,22 +0,0 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import DeleteSpammerModal from 'admin/mixins/delete-spammer-modal';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, DeleteSpammerModal, {
|
||||
removeAfter: null,
|
||||
|
||||
actions: {
|
||||
deletePostDeferFlag() {
|
||||
let flaggedPost = this.get('model');
|
||||
this.removeAfter(flaggedPost.deferFlags(true)).then(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
},
|
||||
|
||||
deletePostAgreeFlag() {
|
||||
let flaggedPost = this.get('model');
|
||||
this.removeAfter(flaggedPost.agreeFlags('delete')).then(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,50 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
silenceUntil: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
silencing: false,
|
||||
user: null,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
|
||||
onShow() {
|
||||
this.setProperties({
|
||||
silenceUntil: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
silencing: false,
|
||||
loadingUser: true,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
});
|
||||
},
|
||||
|
||||
@computed('silenceUntil', 'reason', 'silencing')
|
||||
submitDisabled(silenceUntil, reason, silencing) {
|
||||
return (silencing || Ember.isEmpty(silenceUntil) || !reason || reason.length < 1);
|
||||
},
|
||||
|
||||
actions: {
|
||||
silence() {
|
||||
if (this.get('submitDisabled')) { return; }
|
||||
|
||||
this.set('silencing', true);
|
||||
this.get('user').silence({
|
||||
silenced_till: this.get('silenceUntil'),
|
||||
reason: this.get('reason'),
|
||||
message: this.get('message'),
|
||||
post_id: this.get('post.id')
|
||||
}).then(result => {
|
||||
this.send('closeModal');
|
||||
let callback = this.get('successCallback');
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
}).catch(popupAjaxError).finally(() => this.set('silencing', false));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,23 +0,0 @@
|
||||
export default Ember.Mixin.create({
|
||||
adminTools: Ember.inject.service(),
|
||||
spammerDetails: null,
|
||||
|
||||
onShow() {
|
||||
let adminTools = this.get('adminTools');
|
||||
let spammerDetails = adminTools.spammerDetails(this.get('model.user'));
|
||||
|
||||
this.setProperties({
|
||||
spammerDetails,
|
||||
canDeleteSpammer: spammerDetails.canDelete && this.get('model.flaggedForSpam')
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
deleteSpammer() {
|
||||
let spammerDetails = this.get('spammerDetails');
|
||||
this.removeAfter(spammerDetails.deleteUser()).then(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -8,6 +8,8 @@ import Group from 'discourse/models/group';
|
||||
import TL3Requirements from 'admin/models/tl3-requirements';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
|
||||
const wrapAdmin = user => user ? AdminUser.create(user) : null;
|
||||
|
||||
const AdminUser = Discourse.User.extend({
|
||||
adminUserView: true,
|
||||
customGroups: Ember.computed.filter("groups", g => !g.automatic && Group.create(g)),
|
||||
@ -232,6 +234,7 @@ const AdminUser = Discourse.User.extend({
|
||||
}.property('trust_level'),
|
||||
|
||||
isSuspended: Em.computed.equal('suspended', true),
|
||||
isSilenced: Ember.computed.equal('silenced', true),
|
||||
canSuspend: Em.computed.not('staff'),
|
||||
|
||||
suspendDuration: function() {
|
||||
@ -299,46 +302,38 @@ const AdminUser = Discourse.User.extend({
|
||||
});
|
||||
},
|
||||
|
||||
unblock() {
|
||||
this.set('blockingUser', true);
|
||||
return ajax('/admin/users/' + this.id + '/unblock', {
|
||||
unsilence() {
|
||||
this.set('silencingUser', true);
|
||||
|
||||
return ajax(`/admin/users/${this.id}/unsilence`, {
|
||||
type: 'PUT'
|
||||
}).then(function() {
|
||||
window.location.reload();
|
||||
}).catch(function(e) {
|
||||
var error = I18n.t('admin.user.unblock_failed', { error: "http: " + e.status + " - " + e.body });
|
||||
}).then(result => {
|
||||
this.setProperties(result.unsilence);
|
||||
}).catch(e => {
|
||||
let error = I18n.t('admin.user.unsilence_failed', {
|
||||
error: `http: ${e.status} - ${e.body}`
|
||||
});
|
||||
bootbox.alert(error);
|
||||
}).finally(() => {
|
||||
this.set('silencingUser', false);
|
||||
});
|
||||
},
|
||||
|
||||
block() {
|
||||
const user = this,
|
||||
message = I18n.t("admin.user.block_confirm");
|
||||
|
||||
const performBlock = function() {
|
||||
user.set('blockingUser', true);
|
||||
return ajax('/admin/users/' + user.id + '/block', {
|
||||
type: 'PUT'
|
||||
}).then(function() {
|
||||
window.location.reload();
|
||||
}).catch(function(e) {
|
||||
var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body });
|
||||
bootbox.alert(error);
|
||||
user.set('blockingUser', false);
|
||||
silence(data) {
|
||||
this.set('silencingUser', true);
|
||||
return ajax(`/admin/users/${this.id}/silence`, {
|
||||
type: 'PUT',
|
||||
data
|
||||
}).then(result => {
|
||||
this.setProperties(result.silence);
|
||||
}).catch(e => {
|
||||
let error = I18n.t('admin.user.silence_failed', {
|
||||
error: `http: ${e.status} - ${e.body}`
|
||||
});
|
||||
};
|
||||
|
||||
const buttons = [{
|
||||
"label": I18n.t("composer.cancel"),
|
||||
"class": "cancel",
|
||||
"link": true
|
||||
}, {
|
||||
"label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.block_accept'),
|
||||
"class": "btn btn-danger",
|
||||
"callback": function() { performBlock(); }
|
||||
}];
|
||||
|
||||
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
|
||||
bootbox.alert(error);
|
||||
}).finally(() => {
|
||||
this.set('silencingUser', false);
|
||||
});
|
||||
},
|
||||
|
||||
sendActivationEmail() {
|
||||
@ -475,17 +470,14 @@ const AdminUser = Discourse.User.extend({
|
||||
}
|
||||
}.property('tl3_requirements'),
|
||||
|
||||
suspendedBy: function() {
|
||||
if (this.get('suspended_by')) {
|
||||
return AdminUser.create(this.get('suspended_by'));
|
||||
}
|
||||
}.property('suspended_by'),
|
||||
@computed('suspended_by')
|
||||
suspendedBy: wrapAdmin,
|
||||
|
||||
approvedBy: function() {
|
||||
if (this.get('approved_by')) {
|
||||
return AdminUser.create(this.get('approved_by'));
|
||||
}
|
||||
}.property('approved_by')
|
||||
@computed('silenced_by')
|
||||
silencedBy: wrapAdmin,
|
||||
|
||||
@computed('approved_by')
|
||||
approvedBy: wrapAdmin,
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -8,18 +8,6 @@ const VersionCheck = Discourse.Model.extend({
|
||||
return updatedAt === null;
|
||||
},
|
||||
|
||||
@computed('updated_at', 'version_check_pending')
|
||||
dataIsOld(updatedAt, versionCheckPending) {
|
||||
return versionCheckPending || moment().diff(moment(updatedAt), 'hours') >= 48;
|
||||
},
|
||||
|
||||
@computed('dataIsOld', 'installed_version', 'latest_version', 'missing_versions_count')
|
||||
staleData(dataIsOld, installedVersion, latestVersion, missingVersionsCount) {
|
||||
return dataIsOld ||
|
||||
(installedVersion !== latestVersion && missingVersionsCount === 0) ||
|
||||
(installedVersion === latestVersion && missingVersionsCount !== 0);
|
||||
},
|
||||
|
||||
@computed('missing_versions_count')
|
||||
upToDate(missingVersionsCount) {
|
||||
return missingVersionsCount === 0 || missingVersionsCount === null;
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
renderTemplate() {
|
||||
this.render('admin/templates/logs/search-logs', {into: 'adminLogs'});
|
||||
},
|
||||
|
||||
queryParams: {
|
||||
period: {
|
||||
refreshModel: true
|
||||
}
|
||||
},
|
||||
|
||||
model(params) {
|
||||
this._params = params;
|
||||
return ajax('/admin/logs/search_logs.json', { data: { period: params.period } }).then(search_logs => {
|
||||
return search_logs.map(sl => Ember.Object.create(sl));
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
const params = this._params;
|
||||
controller.setProperties({ model, period: params.period });
|
||||
}
|
||||
});
|
||||
@ -66,6 +66,7 @@ export default function() {
|
||||
this.route('screenedEmails', { path: '/screened_emails' });
|
||||
this.route('screenedIpAddresses', { path: '/screened_ip_addresses' });
|
||||
this.route('screenedUrls', { path: '/screened_urls' });
|
||||
this.route('searchLogs', { path: '/search_logs' });
|
||||
this.route('adminWatchedWords', { path: '/watched_words', resetNamespace: true}, function() {
|
||||
this.route('index', { path: '/' } );
|
||||
this.route('action', { path: '/action/:action_id' } );
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
export default Discourse.Route.extend({
|
||||
redirect: function() {
|
||||
this.transitionTo('adminUsersList');
|
||||
}
|
||||
});
|
||||
@ -6,9 +6,17 @@ import AdminUser from 'admin/models/admin-user';
|
||||
import { iconHTML } from 'discourse-common/lib/icon-library';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
|
||||
export default Ember.Service.extend({
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
|
||||
// TODO: Make `siteSettings` a service that can be injected
|
||||
this.siteSettings = getOwner(this).lookup('site-settings:main');
|
||||
},
|
||||
|
||||
checkSpammer(userId) {
|
||||
return AdminUser.find(userId).then(au => this.spammerDetails(au));
|
||||
},
|
||||
@ -20,12 +28,12 @@ export default Ember.Service.extend({
|
||||
};
|
||||
},
|
||||
|
||||
showSuspendModal(user, opts) {
|
||||
_showControlModal(type, user, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
let controller = showModal('admin-suspend-user', {
|
||||
let controller = showModal(`admin-${type}-user`, {
|
||||
admin: true,
|
||||
modalClass: 'suspend-user-modal'
|
||||
modalClass: `${type}-user-modal`
|
||||
});
|
||||
if (opts.post) {
|
||||
controller.set('post', opts.post);
|
||||
@ -44,8 +52,22 @@ export default Ember.Service.extend({
|
||||
});
|
||||
},
|
||||
|
||||
showSilenceModal(user, opts) {
|
||||
this._showControlModal('silence', user, opts);
|
||||
},
|
||||
|
||||
showSuspendModal(user, opts) {
|
||||
this._showControlModal('suspend', user, opts);
|
||||
},
|
||||
|
||||
_deleteSpammer(adminUser) {
|
||||
return adminUser.checkEmail().then(() => {
|
||||
|
||||
// Try loading the email if the site supports it
|
||||
let tryEmail = this.siteSettings.show_email_on_profile ?
|
||||
adminUser.checkEmail() :
|
||||
Ember.RSVP.resolve();
|
||||
|
||||
return tryEmail.then(() => {
|
||||
|
||||
let message = I18n.messageFormat('flagging.delete_confirm_MF', {
|
||||
"POSTS": adminUser.get('post_count'),
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
</ul>
|
||||
|
||||
<div class='boxed white admin-content'>
|
||||
<div class='admin-contents'>
|
||||
<div class='admin-contents {{adminContentsClassName}}'>
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
<div class="api">
|
||||
{{#admin-nav}}
|
||||
{{nav-item route='adminApiKeys' label='admin.api.title'}}
|
||||
{{nav-item route='adminWebHooks' label='admin.web_hooks.title'}}
|
||||
{{/admin-nav}}
|
||||
{{#admin-nav}}
|
||||
{{nav-item route='adminApiKeys' label='admin.api.title'}}
|
||||
{{nav-item route='adminWebHooks' label='admin.web_hooks.title'}}
|
||||
{{/admin-nav}}
|
||||
|
||||
<div class="admin-container">
|
||||
{{outlet}}
|
||||
</div>
|
||||
<div class="admin-container">
|
||||
{{outlet}}
|
||||
</div>
|
||||
|
||||
@ -20,6 +20,10 @@
|
||||
</div>
|
||||
|
||||
<div class="flagged-post-contents">
|
||||
<div class='flagged-post-user-details'>
|
||||
<a class='username' href={{user.path}} data-user-card={{flaggedPost.user.username}}>{{format-username flaggedPost.user.username}}</a>
|
||||
</div>
|
||||
|
||||
<div class='flagged-post-excerpt'>
|
||||
{{#unless hideTitle}}
|
||||
<h3>
|
||||
@ -107,13 +111,9 @@
|
||||
|
||||
{{#if canAct}}
|
||||
<div class='flagged-post-controls'>
|
||||
{{d-button
|
||||
title="admin.flags.agree_title"
|
||||
class="agree-flag"
|
||||
label="admin.flags.agree"
|
||||
icon="thumbs-o-up"
|
||||
action="showAgreeFlagModal"
|
||||
ellipsis=true}}
|
||||
{{admin-agree-flag-dropdown
|
||||
post=flaggedPost
|
||||
removeAfter=(action "removeAfter") }}
|
||||
|
||||
{{#if flaggedPost.postHidden}}
|
||||
{{d-button
|
||||
@ -138,12 +138,7 @@
|
||||
icon="external-link"
|
||||
label="admin.flags.defer_flag"}}
|
||||
|
||||
{{d-button
|
||||
class="btn-danger delete-flag"
|
||||
title="admin.flags.delete_title"
|
||||
action="showDeleteFlagModal"
|
||||
icon="trash-o"
|
||||
label="admin.flags.delete"}}
|
||||
{{admin-delete-flag-dropdown post=flaggedPost removeAfter=(action "removeAfter")}}
|
||||
|
||||
{{#unless suspended}}
|
||||
{{d-button
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{#each users as |u|}}
|
||||
{{#link-to 'adminUser' u class="flagged-topic-user"}}
|
||||
{{#link-to 'adminUser' u.id u.username class="flagged-topic-user"}}
|
||||
{{avatar u imageSize="small"}}
|
||||
{{/link-to}}
|
||||
{{/each}}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
{{category-selector categories=selectedCategories blacklist=selectedCategories}}
|
||||
{{category-selector categories=selectedCategories}}
|
||||
<div class='desc'>{{{unbound setting.description}}}</div>
|
||||
{{setting-validation-message message=validationMessage}}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{{combo-box valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}}
|
||||
{{combo-box castInteger=true valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}}
|
||||
{{preview}}
|
||||
{{setting-validation-message message=validationMessage}}
|
||||
<div class='desc'>{{{unbound setting.description}}}</div>
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
<div class='customize'>
|
||||
{{#admin-nav}}
|
||||
{{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}}
|
||||
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
|
||||
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
|
||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
||||
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
|
||||
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
|
||||
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
|
||||
{{nav-item route='adminEmbedding' label='admin.embedding.title'}}
|
||||
{{/admin-nav}}
|
||||
{{#admin-nav}}
|
||||
{{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}}
|
||||
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
|
||||
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
|
||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
||||
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
|
||||
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
|
||||
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
|
||||
{{nav-item route='adminEmbedding' label='admin.embedding.title'}}
|
||||
{{/admin-nav}}
|
||||
|
||||
<div class="admin-container">
|
||||
{{outlet}}
|
||||
</div>
|
||||
<div class="admin-container">
|
||||
{{outlet}}
|
||||
</div>
|
||||
|
||||
@ -37,8 +37,8 @@
|
||||
<tr>
|
||||
<td class="title">{{d-icon "shield"}} {{i18n 'admin.dashboard.moderators'}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'moderators'}}{{moderators}}{{/link-to}}</td>
|
||||
<td class="title">{{d-icon "ban"}} {{i18n 'admin.dashboard.blocked'}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'blocked'}}{{blocked}}{{/link-to}}</td>
|
||||
<td class="title">{{d-icon "ban"}} {{i18n 'admin.dashboard.silenced'}}</td>
|
||||
<td class="value">{{#link-to 'adminUsersList.show' 'silenced'}}{{silenced}}{{/link-to}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -37,7 +37,8 @@
|
||||
ft.id
|
||||
class="btn d-button no-text btn-small btn-primary show-details"
|
||||
title=(i18n "admin.flags.show_details")}}
|
||||
{{d-icon "search"}}
|
||||
{{d-icon "list"}}
|
||||
{{i18n "admin.flags.details"}}
|
||||
{{/link-to}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
{{nav-item route='adminLogs.screenedIpAddresses' label='admin.logs.screened_ips.title'}}
|
||||
{{nav-item route='adminLogs.screenedUrls' label='admin.logs.screened_urls.title'}}
|
||||
{{nav-item route='adminWatchedWords' label='admin.watched_words.title'}}
|
||||
{{nav-item route='adminLogs.searchLogs' label='admin.logs.search_logs.title'}}
|
||||
{{#if currentUser.admin}}
|
||||
{{nav-item path='/logs' label='admin.logs.logster.title'}}
|
||||
{{/if}}
|
||||
|
||||
36
app/assets/javascripts/admin/templates/logs/search-logs.hbs
Normal file
36
app/assets/javascripts/admin/templates/logs/search-logs.hbs
Normal file
@ -0,0 +1,36 @@
|
||||
<p>
|
||||
{{period-chooser period=period}}
|
||||
</p>
|
||||
<br>
|
||||
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
{{#if model.length}}
|
||||
|
||||
<div class='table search-logs-list'>
|
||||
<div class="heading-container">
|
||||
<div class="col heading term">{{i18n 'admin.logs.search_logs.term'}}</div>
|
||||
<div class="col heading">{{i18n 'admin.logs.search_logs.searches'}}</div>
|
||||
<div class="col heading">{{i18n 'admin.logs.search_logs.click_through'}}</div>
|
||||
<div class="col heading topic">{{i18n 'admin.logs.search_logs.most_viewed_topic'}}</div>
|
||||
<div class="col heading" title="{{i18n 'admin.logs.search_logs.unique_title'}}">{{i18n 'admin.logs.search_logs.unique'}}</div>
|
||||
</div>
|
||||
|
||||
{{#each model as |item|}}
|
||||
<div class="admin-list-item">
|
||||
<div class="col term">{{item.term}}</div>
|
||||
<div class="col">{{item.searches}}</div>
|
||||
<div class="col">{{item.click_through}}</div>
|
||||
<div class="col topic">
|
||||
{{#if item.clicked_topic_id}}
|
||||
<a href='{{unbound item.topic_url}}'>{{item.topic_title}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="col">{{item.unique}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
{{i18n 'search.no_results'}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="staff-action-logs-controls">
|
||||
{{#if filtersExists}}
|
||||
<div>
|
||||
<div class='staff-action-logs-filters'>
|
||||
<a {{action "clearAllFilters"}} class="clear-filters filter">
|
||||
<span class="label">{{i18n 'admin.logs.staff_actions.clear_filters'}}</span>
|
||||
</a>
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
{{#d-modal-body title="admin.flags.agree_flag_modal_title"}}
|
||||
{{#if model.user_deleted}}
|
||||
{{d-button
|
||||
title="admin.flags.agree_flag_restore_post_title"
|
||||
class="confirm-agree-restore"
|
||||
action=(action "perform" "restore")
|
||||
icon="eye"
|
||||
label="admin.flags.agree_flag_restore_post"}}
|
||||
{{else}}
|
||||
{{#unless model.postHidden}}
|
||||
{{d-button
|
||||
title="admin.flags.agree_flag_hide_post_title"
|
||||
action=(action "perform" "hide")
|
||||
class="confirm-agree-hide"
|
||||
icon="eye-slash"
|
||||
label="admin.flags.agree_flag_hide_post"}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{d-button
|
||||
title="admin.flags.agree_flag_title"
|
||||
action=(action "perform" "keep")
|
||||
class="confirm-agree-keep"
|
||||
icon="thumbs-o-up"
|
||||
label="admin.flags.agree_flag"}}
|
||||
|
||||
{{#if canDeleteSpammer}}
|
||||
{{d-button
|
||||
title="admin.flags.delete_spammer_title"
|
||||
action="deleteSpammer"
|
||||
class="btn-danger delete-spammer"
|
||||
icon="exclamation-triangle"
|
||||
label="admin.flags.delete_spammer"}}
|
||||
{{/if}}
|
||||
{{/d-modal-body}}
|
||||
@ -1,24 +0,0 @@
|
||||
{{#d-modal-body title="admin.flags.delete_flag_modal_title"}}
|
||||
{{d-button
|
||||
class="delete-defer"
|
||||
title="admin.flags.delete_post_defer_flag_title"
|
||||
action="deletePostDeferFlag"
|
||||
icon="external-link"
|
||||
label="admin.flags.delete_post_defer_flag"}}
|
||||
|
||||
{{d-button
|
||||
class="delete-agree"
|
||||
title="admin.flags.delete_post_agree_flag_title"
|
||||
action="deletePostAgreeFlag"
|
||||
icon="thumbs-o-up"
|
||||
label="admin.flags.delete_post_agree_flag"}}
|
||||
|
||||
{{#if canDeleteSpammer}}
|
||||
{{d-button
|
||||
class="btn-danger delete-spammer"
|
||||
title="admin.flags.delete_spammer_title"
|
||||
action="deleteSpammer"
|
||||
icon="exclamation-triangle"
|
||||
label="admin.flags.delete_spammer"}}
|
||||
{{/if}}
|
||||
{{/d-modal-body}}
|
||||
@ -0,0 +1,50 @@
|
||||
{{#d-modal-body title="admin.user.silence_modal_title"}}
|
||||
{{#conditional-loading-spinner condition=loadingUser}}
|
||||
|
||||
<div class='until-controls'>
|
||||
<label>
|
||||
{{future-date-input
|
||||
class="silence-until"
|
||||
label="admin.user.silence_duration"
|
||||
includeFarFuture=true
|
||||
input=silenceUntil}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class='reason-controls'>
|
||||
<label>
|
||||
<div class='silence-reason-label'>
|
||||
{{{i18n 'admin.user.silence_reason_label'}}}
|
||||
</div>
|
||||
</label>
|
||||
{{text-field
|
||||
value=reason
|
||||
class="silence-reason"
|
||||
placeholderKey="admin.user.silence_reason_placeholder"}}
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<div class='silence-message-label'>
|
||||
{{i18n "admin.user.silence_message"}}
|
||||
</div>
|
||||
</label>
|
||||
{{textarea
|
||||
value=message
|
||||
class="silence-message"
|
||||
placeholder=(i18n "admin.user.silence_message_placeholder")}}
|
||||
|
||||
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button
|
||||
class="btn-danger perform-silence"
|
||||
action="silence"
|
||||
disabled=submitDisabled
|
||||
icon="microphone-slash"
|
||||
label="admin.user.silence"}}
|
||||
{{d-modal-cancel close=(action "closeModal")}}
|
||||
{{conditional-loading-spinner condition=loading size="small"}}
|
||||
</div>
|
||||
@ -2,5 +2,5 @@
|
||||
<pre>{{model.details}}</pre>
|
||||
{{/d-modal-body}}
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action "closeModal"}}>{{i18n 'close'}}</button>
|
||||
{{d-button action=(action "closeModal") label="close"}}
|
||||
</div>
|
||||
|
||||
@ -21,23 +21,22 @@
|
||||
{{{i18n 'admin.user.suspend_reason_label'}}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{text-field
|
||||
value=reason
|
||||
class="suspend-reason"
|
||||
placeholderKey="admin.user.suspend_reason_placeholder"}}
|
||||
</label>
|
||||
{{text-field
|
||||
value=reason
|
||||
class="suspend-reason"
|
||||
placeholderKey="admin.user.suspend_reason_placeholder"}}
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<div class='suspend-message-label'>
|
||||
{{i18n "admin.user.suspend_message"}}
|
||||
</div>
|
||||
{{textarea
|
||||
</label>
|
||||
{{textarea
|
||||
value=message
|
||||
class="suspend-message"
|
||||
placeholder=(i18n "admin.user.suspend_message_placeholder")}}
|
||||
</label>
|
||||
{{else}}
|
||||
<div class='cant-suspend'>
|
||||
{{i18n "admin.user.cant_suspend"}}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
{{d-button action="logOut" icon="power-off" label="admin.user.log_out"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{plugin-outlet name="admin-user-controls-after" args=(hash model=model) tagName="" connectorTagName=""}}
|
||||
</div>
|
||||
|
||||
<div class='display-row username'>
|
||||
@ -287,7 +288,7 @@
|
||||
</div>
|
||||
<div class="controls">
|
||||
{{#if model.canLockTrustLevel}}
|
||||
{{#if model.trust_level_locked}}
|
||||
{{#if model.manual_locked_trust_level}}
|
||||
{{d-icon "lock" title="admin.user.trust_level_locked_tip"}}
|
||||
{{d-button action="lockTrustLevel" actionParam=false label="admin.user.unlock_trust_level"}}
|
||||
{{else}}
|
||||
@ -346,21 +347,51 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="display-row {{if model.blocked 'highlight-danger'}}">
|
||||
<div class='field'>{{i18n 'admin.user.blocked'}}</div>
|
||||
<div class='value'>{{i18n-yes-no model.blocked}}</div>
|
||||
<div class="display-row {{if model.silenced 'highlight-danger'}}">
|
||||
<div class='field'>{{i18n 'admin.user.silenced'}}</div>
|
||||
<div class='value'>
|
||||
{{i18n-yes-no model.silenced}}
|
||||
{{#if model.isSilenced}}
|
||||
{{#unless model.silencedForever}}
|
||||
{{i18n "admin.user.suspended_until" until=model.silencedTillDate}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class='controls'>
|
||||
{{#conditional-loading-spinner size="small" condition=model.blockingUser}}
|
||||
{{#if model.blocked}}
|
||||
{{d-button action="unblock" icon="thumbs-o-up" label="admin.user.unblock"}}
|
||||
{{i18n 'admin.user.block_explanation'}}
|
||||
{{#conditional-loading-spinner size="small" condition=model.silencingUser}}
|
||||
{{#if model.silenced}}
|
||||
{{d-button
|
||||
class="btn-danger unsilence-user"
|
||||
action="unsilence"
|
||||
icon="microphone-slash"
|
||||
label="admin.user.unsilence"}}
|
||||
{{i18n 'admin.user.silence_explanation'}}
|
||||
{{else}}
|
||||
{{d-button action="block" icon="ban" label="admin.user.block"}}
|
||||
{{i18n 'admin.user.block_explanation'}}
|
||||
{{d-button
|
||||
class="btn-danger silence-user"
|
||||
action=(action "showSilenceModal")
|
||||
icon="microphone-slash"
|
||||
label="admin.user.silence"}}
|
||||
{{i18n 'admin.user.silence_explanation'}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if model.isSilenced}}
|
||||
<div class='display-row highlight-danger silence-info'>
|
||||
<div class='field'>{{i18n 'admin.user.silenced_by'}}</div>
|
||||
<div class='value'>
|
||||
{{#link-to 'adminUser' silencedBy}}{{avatar model.silencedBy imageSize="tiny"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' silencedBy}}{{model.silencedBy.username}}{{/link-to}}
|
||||
</div>
|
||||
<div class='controls'>
|
||||
<b>{{i18n 'admin.user.silence_reason'}}</b>:
|
||||
{{model.silence_reason}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
</section>
|
||||
|
||||
{{#if currentUser.admin}}
|
||||
@ -398,7 +429,7 @@
|
||||
</div>
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.users.last_emailed'}}</div>
|
||||
<div class='value'>{{format-date model.last_emailed_at leaveAgo="true"}}</div>
|
||||
<div class='value'>{{format-date model.last_emailed_at}}</div>
|
||||
</div>
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'last_seen'}}</div>
|
||||
@ -443,7 +474,7 @@
|
||||
</div>
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.user.time_read'}}</div>
|
||||
<div class='value'>{{{model.time_read}}}</div>
|
||||
<div class='value'>{{{format-duration model.time_read}}}</div>
|
||||
</div>
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'user.invited.days_visited'}}</div>
|
||||
|
||||
@ -53,17 +53,20 @@
|
||||
{{avatar user imageSize="small"}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<td class="username">
|
||||
{{#link-to 'adminUser' user}}{{unbound user.username}}{{/link-to}}
|
||||
{{#if user.staged}}
|
||||
{{d-icon "envelope-o" title="user.staged" }}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class='email'>
|
||||
{{unbound user.email}}
|
||||
</td>
|
||||
<td>
|
||||
{{{unbound user.last_emailed_age}}}
|
||||
{{{format-duration user.last_emailed_age}}}
|
||||
</td>
|
||||
<td>
|
||||
{{{unbound user.last_seen_age}}}
|
||||
{{{format-duration user.last_seen_age}}}
|
||||
</td>
|
||||
<td>
|
||||
{{number user.topics_entered}}
|
||||
@ -72,11 +75,11 @@
|
||||
{{number user.posts_read_count}}
|
||||
</td>
|
||||
<td>
|
||||
{{{unbound user.time_read}}}
|
||||
{{{format-duration user.time_read}}}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{{unbound user.created_at_age}}}
|
||||
{{{format-duration user.created_at_age}}}
|
||||
</td>
|
||||
|
||||
{{#if showApproval}}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
{{/if}}
|
||||
{{nav-item route='adminUsersList.show' routeParam='staff' label='admin.users.nav.staff'}}
|
||||
{{nav-item route='adminUsersList.show' routeParam='suspended' label='admin.users.nav.suspended'}}
|
||||
{{nav-item route='adminUsersList.show' routeParam='blocked' label='admin.users.nav.blocked'}}
|
||||
{{nav-item route='adminUsersList.show' routeParam='silenced' label='admin.users.nav.silenced'}}
|
||||
{{nav-item route='adminUsersList.show' routeParam='suspect' label='admin.users.nav.suspect'}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<span class="normal-note">{{i18n 'admin.dashboard.no_check_performed'}}</span>
|
||||
</td>
|
||||
{{else}}
|
||||
{{#if versionCheck.staleData}}
|
||||
{{#if versionCheck.stale_data}}
|
||||
<td class="version-number">{{#if versionCheck.version_check_pending}}{{dash-if-empty versionCheck.installed_version}}{{/if}}</td>
|
||||
<td class="face">
|
||||
{{#if versionCheck.version_check_pending}}
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
<div class='filters'>
|
||||
<div>
|
||||
<label>{{d-icon 'circle' class='tracking'}}{{i18n 'admin.web_hooks.categories_filter'}}</label>
|
||||
{{category-selector categories=model.categories blacklist=model.categories}}
|
||||
{{category-selector categories=model.categories}}
|
||||
<div class="instructions">{{i18n 'admin.web_hooks.categories_filter_instructions'}}</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
//= require ./ember-addons/ember-computed-decorators
|
||||
//= require ./ember-addons/fmt
|
||||
//= require_tree ./discourse-common
|
||||
//= require_tree ./select-box-kit
|
||||
//= require_tree ./select-kit
|
||||
//= require ./discourse
|
||||
//= require ./deprecated
|
||||
|
||||
|
||||
@ -42,7 +42,8 @@ export function renderIcon(renderType, id, params) {
|
||||
let rendererForType = renderer[renderType];
|
||||
|
||||
if (rendererForType) {
|
||||
let result = rendererForType(REPLACEMENTS[id] || id, params || {});
|
||||
const icon = { id, replacementId: REPLACEMENTS[id] };
|
||||
let result = rendererForType(icon, params || {});
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
@ -68,8 +69,9 @@ export function registerIconRenderer(renderer) {
|
||||
}
|
||||
|
||||
// Support for font awesome icons
|
||||
function faClasses(id, params) {
|
||||
let classNames = `fa fa-${id} d-icon d-icon-${id}`;
|
||||
function faClasses(icon, params) {
|
||||
let classNames = `fa fa-${icon.replacementId || icon.id} d-icon d-icon-${icon.id}`;
|
||||
|
||||
if (params) {
|
||||
if (params.modifier) { classNames += " fa-" + params.modifier; }
|
||||
if (params['class']) { classNames += ' ' + params['class']; }
|
||||
@ -81,9 +83,9 @@ function faClasses(id, params) {
|
||||
registerIconRenderer({
|
||||
name: 'font-awesome',
|
||||
|
||||
string(id, params) {
|
||||
string(icon, params) {
|
||||
let tagName = params.tagName || 'i';
|
||||
let html = `<${tagName} class='${faClasses(id, params)}'`;
|
||||
let html = `<${tagName} class='${faClasses(icon, params)}'`;
|
||||
if (params.title) { html += ` title='${I18n.t(params.title)}'`; }
|
||||
if (params.label) { html += " aria-hidden='true'"; }
|
||||
html += `></${tagName}>`;
|
||||
@ -93,11 +95,11 @@ registerIconRenderer({
|
||||
return html;
|
||||
},
|
||||
|
||||
node(id, params) {
|
||||
node(icon, params) {
|
||||
let tagName = params.tagName || 'i';
|
||||
|
||||
const properties = {
|
||||
className: faClasses(id, params),
|
||||
className: faClasses(icon, params),
|
||||
attributes: { "aria-hidden": true }
|
||||
};
|
||||
|
||||
|
||||
@ -11,9 +11,11 @@ export default Ember.Component.extend(BadgeSelectController, {
|
||||
save() {
|
||||
this.setProperties({ saved: false, saving: true });
|
||||
|
||||
var badge_id = this.get('selectedUserBadgeId') || 0;
|
||||
|
||||
ajax(this.get('user.path') + "/preferences/badge_title", {
|
||||
type: "PUT",
|
||||
data: { user_badge_id: this.get('selectedUserBadgeId') }
|
||||
data: { user_badge_id: badge_id }
|
||||
}).then(() => {
|
||||
this.setProperties({
|
||||
saved: true,
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
||||
import Category from 'discourse/models/category';
|
||||
import { on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
@observes('categories')
|
||||
_update() {
|
||||
if (this.get('canReceiveUpdates') === 'true')
|
||||
this._initializeAutocomplete({updateData: true});
|
||||
},
|
||||
|
||||
@on('didInsertElement')
|
||||
_initializeAutocomplete(opts) {
|
||||
const self = this,
|
||||
regexp = new RegExp(`href=['\"]${Discourse.getURL('/c/')}([^'\"]+)`);
|
||||
|
||||
this.$('input').autocomplete({
|
||||
items: this.get('categories'),
|
||||
single: this.get('single'),
|
||||
allowAny: false,
|
||||
updateData: (opts && opts.updateData) ? opts.updateData : false,
|
||||
dataSource(term) {
|
||||
return Category.list().filter(category => {
|
||||
const regex = new RegExp(term, 'i');
|
||||
return category.get('name').match(regex) &&
|
||||
!_.contains(self.get('blacklist') || [], category) &&
|
||||
!_.contains(self.get('categories'), category) ;
|
||||
});
|
||||
},
|
||||
onChangeItems(items) {
|
||||
const categories = _.map(items, link => {
|
||||
const slug = link.match(regexp)[1];
|
||||
return Category.findSingleBySlug(slug);
|
||||
});
|
||||
Em.run.next(() => {
|
||||
let existingCategory = _.isArray(self.get('categories')) ? self.get('categories') : [self.get('categories')];
|
||||
const result = _.intersection(existingCategory.map(itm => itm.id), categories.map(itm => itm.id));
|
||||
if (result.length !== categories.length || existingCategory.length !== categories.length)
|
||||
self.set('categories', categories);
|
||||
});
|
||||
},
|
||||
template: findRawTemplate('category-selector-autocomplete'),
|
||||
transformComplete(category) {
|
||||
return categoryBadgeHTML(category, {allowUncategorized: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -15,6 +15,7 @@ export default Ember.Component.extend(KeyEnterEscape, {
|
||||
'composer.createdPost:created-post',
|
||||
'composer.creatingTopic:topic',
|
||||
'composer.whisper:composing-whisper',
|
||||
'showPreview:show-preview:hide-preview',
|
||||
'currentUserPrimaryGroupClass'],
|
||||
|
||||
@computed("currentUser.primary_group_name")
|
||||
@ -41,19 +42,6 @@ export default Ember.Component.extend(KeyEnterEscape, {
|
||||
|
||||
const h = $('#reply-control').height() || 0;
|
||||
this.movePanels(h + "px");
|
||||
|
||||
// Figure out the size of the fields
|
||||
const $fields = this.$('.composer-fields');
|
||||
const fieldPos = $fields.position();
|
||||
if (fieldPos) {
|
||||
this.$('.wmd-controls').css('top', $fields.height() + fieldPos.top + 5);
|
||||
}
|
||||
|
||||
// get the submit panel height
|
||||
const submitPos = this.$('.submit-panel').position();
|
||||
if (submitPos) {
|
||||
this.$('.wmd-controls').css('bottom', h - submitPos.top + 7);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import userSearch from 'discourse/lib/user-search';
|
||||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
|
||||
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
|
||||
import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags';
|
||||
import { linkSeenTagHashtags, fetchUnseenTagHashtags } from 'discourse/lib/link-tag-hashtag';
|
||||
@ -12,58 +12,36 @@ import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
import { tinyAvatar,
|
||||
displayErrorForUpload,
|
||||
getUploadMarkdown,
|
||||
validateUploadedFiles } from 'discourse/lib/utilities';
|
||||
import { lookupCachedUploadUrl,
|
||||
lookupUncachedUploadUrls,
|
||||
cacheShortUploadUrl } from 'pretty-text/image-short-url';
|
||||
validateUploadedFiles,
|
||||
formatUsername
|
||||
} from 'discourse/lib/utilities';
|
||||
import { cacheShortUploadUrl, resolveAllShortUrls } from 'pretty-text/image-short-url';
|
||||
|
||||
const REBUILD_SCROLL_MAP_EVENTS = [
|
||||
'composer:resized',
|
||||
'composer:typed-reply'
|
||||
];
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['wmd-controls'],
|
||||
classNameBindings: ['showToolbar:toolbar-visible', ':wmd-controls', 'showPreview', 'showPreview::hide-preview'],
|
||||
classNameBindings: ['showToolbar:toolbar-visible', ':wmd-controls'],
|
||||
|
||||
uploadProgress: 0,
|
||||
showPreview: true,
|
||||
_xhr: null,
|
||||
shouldBuildScrollMap: true,
|
||||
scrollMap: null,
|
||||
|
||||
@computed
|
||||
uploadPlaceholder() {
|
||||
return `[${I18n.t('uploading')}]() `;
|
||||
},
|
||||
|
||||
@on('init')
|
||||
_setupPreview() {
|
||||
const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true'));
|
||||
this.set('showPreview', val === 'true');
|
||||
|
||||
this.appEvents.on('composer:show-preview', () => {
|
||||
this.set('showPreview', true);
|
||||
});
|
||||
|
||||
this.appEvents.on('composer:hide-preview', () => {
|
||||
this.set('showPreview', false);
|
||||
});
|
||||
},
|
||||
|
||||
@computed('site.mobileView', 'showPreview')
|
||||
forcePreview(mobileView, showPreview) {
|
||||
return mobileView && showPreview;
|
||||
},
|
||||
|
||||
@computed('showPreview')
|
||||
toggleText: function(showPreview) {
|
||||
return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview');
|
||||
},
|
||||
|
||||
@observes('showPreview')
|
||||
showPreviewChanged() {
|
||||
this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
|
||||
},
|
||||
|
||||
@computed
|
||||
markdownOptions() {
|
||||
return {
|
||||
previewing: true,
|
||||
|
||||
formatUsername,
|
||||
|
||||
lookupAvatarByPostNumber: (postNumber, topicId) => {
|
||||
const topic = this.get('topic');
|
||||
if (!topic) { return; }
|
||||
@ -75,6 +53,19 @@ export default Ember.Component.extend({
|
||||
return tinyAvatar(quotedPost.get('avatar_template'));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
lookupPrimaryUserGroupByPostNumber: (postNumber, topicId) => {
|
||||
const topic = this.get('topic');
|
||||
if (!topic) { return; }
|
||||
|
||||
const posts = topic.get('postStream.posts');
|
||||
if (posts && topicId === topic.get('id')) {
|
||||
const quotedPost = posts.findBy("post_number", postNumber);
|
||||
if (quotedPost) {
|
||||
return quotedPost.primary_group_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
@ -83,6 +74,8 @@ export default Ember.Component.extend({
|
||||
_composerEditorInit() {
|
||||
const topicId = this.get('topic.id');
|
||||
const $input = this.$('.d-editor-input');
|
||||
const $preview = this.$('.d-editor-preview');
|
||||
|
||||
$input.autocomplete({
|
||||
template: findRawTemplate('user-selector-autocomplete'),
|
||||
dataSource: term => userSearch({
|
||||
@ -94,7 +87,7 @@ export default Ember.Component.extend({
|
||||
transformComplete: v => v.username || v.name
|
||||
});
|
||||
|
||||
$input.on('scroll', () => Ember.run.throttle(this, this._syncEditorAndPreviewScroll, 20));
|
||||
this._initInputPreviewSync($input, $preview);
|
||||
|
||||
// Focus on the body unless we have a title
|
||||
if (!this.get('composer.canEditTitle') && !this.capabilities.isIOS) {
|
||||
@ -134,29 +127,159 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
_syncEditorAndPreviewScroll() {
|
||||
const $input = this.$('.d-editor-input');
|
||||
if (!$input) { return; }
|
||||
_resetShouldBuildScrollMap() {
|
||||
this.set('shouldBuildScrollMap', true);
|
||||
},
|
||||
|
||||
const $preview = this.$('.d-editor-preview');
|
||||
_initInputPreviewSync($input, $preview) {
|
||||
REBUILD_SCROLL_MAP_EVENTS.forEach(event => {
|
||||
this.appEvents.on(event, this, this._resetShouldBuildScrollMap);
|
||||
});
|
||||
|
||||
if ($input.scrollTop() === 0) {
|
||||
$preview.scrollTop(0);
|
||||
return;
|
||||
Ember.run.scheduleOnce("afterRender", () => {
|
||||
$input.on('touchstart mouseenter', () => {
|
||||
if (!$preview.is(":visible")) return;
|
||||
$preview.off('scroll');
|
||||
|
||||
$input.on('scroll', () => {
|
||||
this._syncScroll(this._syncEditorAndPreviewScroll, $input, $preview);
|
||||
});
|
||||
});
|
||||
|
||||
$preview.on('touchstart mouseenter', () => {
|
||||
$input.off('scroll');
|
||||
|
||||
$preview.on('scroll', () => {
|
||||
this._syncScroll(this._syncPreviewAndEditorScroll, $input, $preview);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_syncScroll($callback, $input, $preview) {
|
||||
if (!this.get('scrollMap') || this.get('shouldBuildScrollMap')) {
|
||||
this.set('scrollMap', this._buildScrollMap($input, $preview));
|
||||
this.set('shouldBuildScrollMap', false);
|
||||
}
|
||||
|
||||
const inputHeight = $input[0].scrollHeight;
|
||||
const previewHeight = $preview[0].scrollHeight;
|
||||
if (($input.height() + $input.scrollTop() + 100) > inputHeight) {
|
||||
// cheat, special case for bottom
|
||||
$preview.scrollTop(previewHeight);
|
||||
return;
|
||||
Ember.run.throttle(this, $callback, $input, $preview, this.get('scrollMap'), 20);
|
||||
},
|
||||
|
||||
_teardownInputPreviewSync() {
|
||||
[this.$('.d-editor-input'), this.$('.d-editor-preview')].forEach($element => {
|
||||
$element.off("mouseenter touchstart");
|
||||
$element.off("scroll");
|
||||
});
|
||||
|
||||
REBUILD_SCROLL_MAP_EVENTS.forEach(event => {
|
||||
this.appEvents.off(event, this, this._resetShouldBuildScrollMap);
|
||||
});;
|
||||
},
|
||||
|
||||
// Adapted from https://github.com/markdown-it/markdown-it.github.io
|
||||
_buildScrollMap($input, $preview) {
|
||||
let sourceLikeDiv = $('<div />').css({
|
||||
position: 'absolute',
|
||||
height: 'auto',
|
||||
visibility: 'hidden',
|
||||
width: $input[0].clientWidth,
|
||||
'font-size': $input.css('font-size'),
|
||||
'font-family': $input.css('font-family'),
|
||||
'line-height': $input.css('line-height'),
|
||||
'white-space': $input.css('white-space')
|
||||
}).appendTo('body');
|
||||
|
||||
const linesMap = [];
|
||||
let numberOfLines = 0;
|
||||
|
||||
$input.val().split('\n').forEach(text => {
|
||||
linesMap.push(numberOfLines);
|
||||
|
||||
if (text.length === 0) {
|
||||
numberOfLines++;
|
||||
} else {
|
||||
sourceLikeDiv.text(text);
|
||||
|
||||
let height;
|
||||
let lineHeight;
|
||||
height = parseFloat(sourceLikeDiv.css('height'));
|
||||
lineHeight = parseFloat(sourceLikeDiv.css('line-height'));
|
||||
numberOfLines += Math.round(height / lineHeight);
|
||||
}
|
||||
});
|
||||
|
||||
linesMap.push(numberOfLines);
|
||||
sourceLikeDiv.remove();
|
||||
|
||||
const previewOffsetTop = $preview.offset().top;
|
||||
const offset = $preview.scrollTop() - previewOffsetTop - ($input.offset().top - previewOffsetTop);
|
||||
const nonEmptyList = [];
|
||||
const scrollMap = [];
|
||||
for (let i = 0; i < numberOfLines; i++) { scrollMap.push(-1); };
|
||||
|
||||
nonEmptyList.push(0);
|
||||
scrollMap[0] = 0;
|
||||
|
||||
$preview.find('.preview-sync-line').each((_, element) => {
|
||||
let $element = $(element);
|
||||
let lineNumber = $element.data('line-number');
|
||||
let linesToTop = linesMap[lineNumber];
|
||||
if (linesToTop !== 0) { nonEmptyList.push(linesToTop); }
|
||||
scrollMap[linesToTop] = Math.round($element.offset().top + offset);
|
||||
});
|
||||
|
||||
nonEmptyList.push(numberOfLines);
|
||||
scrollMap[numberOfLines] = $preview[0].scrollHeight;
|
||||
|
||||
let position = 0;
|
||||
|
||||
for (let i = 1; i < numberOfLines; i++) {
|
||||
if (scrollMap[i] !== -1) {
|
||||
position++;
|
||||
continue;
|
||||
}
|
||||
|
||||
let top = nonEmptyList[position];
|
||||
let bottom = nonEmptyList[position + 1];
|
||||
|
||||
scrollMap[i] =
|
||||
((
|
||||
scrollMap[bottom] * (i - top) +
|
||||
scrollMap[top] * (bottom - i)
|
||||
) / (bottom - top)).toFixed(2);
|
||||
};
|
||||
|
||||
return scrollMap;
|
||||
},
|
||||
|
||||
_syncEditorAndPreviewScroll($input, $preview, scrollMap) {
|
||||
let scrollTop;
|
||||
|
||||
if (($input.height() + $input.scrollTop() + 100) > $input[0].scrollHeight) {
|
||||
scrollTop = $preview[0].scrollHeight;
|
||||
} else {
|
||||
const lineHeight = parseFloat($input.css('line-height'));
|
||||
const lineNumber = Math.floor($input.scrollTop() / lineHeight);
|
||||
scrollTop = scrollMap[lineNumber];
|
||||
}
|
||||
|
||||
const scrollPosition = $input.scrollTop();
|
||||
const factor = previewHeight / inputHeight;
|
||||
const desired = scrollPosition * factor;
|
||||
$preview.scrollTop(desired + 50);
|
||||
$preview.stop(true).animate({ scrollTop }, 100, 'linear');
|
||||
},
|
||||
|
||||
_syncPreviewAndEditorScroll($input, $preview, scrollMap) {
|
||||
if (scrollMap.length < 1) return;
|
||||
|
||||
let scrollTop;
|
||||
const previewScrollTop = $preview.scrollTop();
|
||||
|
||||
if (($preview.height() + previewScrollTop + 100) > $preview[0].scrollHeight) {
|
||||
scrollTop = $input[0].scrollHeight;
|
||||
} else {
|
||||
const lineHeight = parseFloat($input.css('line-height'));
|
||||
scrollTop = lineHeight * scrollMap.findIndex(offset => offset > previewScrollTop);
|
||||
}
|
||||
|
||||
$input.stop(true).animate({ scrollTop }, 100, 'linear');
|
||||
},
|
||||
|
||||
_renderUnseenMentions($preview, unseen) {
|
||||
@ -198,24 +321,6 @@ export default Ember.Component.extend({
|
||||
$oneboxes.each((_, o) => load(o, refresh, ajax, this.currentUser.id));
|
||||
},
|
||||
|
||||
_loadShortUrls($images) {
|
||||
const urls = _.map($images, img => $(img).data('orig-src'));
|
||||
lookupUncachedUploadUrls(urls, ajax).then(() => this._loadCachedShortUrls($images));
|
||||
},
|
||||
|
||||
_loadCachedShortUrls($images) {
|
||||
$images.each((idx, image) => {
|
||||
let $image = $(image);
|
||||
let url = lookupCachedUploadUrl($image.data('orig-src'));
|
||||
if (url) {
|
||||
$image.removeAttr('data-orig-src');
|
||||
if (url !== "missing") {
|
||||
$image.attr('src', url);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_warnMentionedGroups($preview) {
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
var found = this.get('warnedGroupMentions') || [];
|
||||
@ -321,6 +426,19 @@ export default Ember.Component.extend({
|
||||
}
|
||||
});
|
||||
|
||||
$element.on("fileuploaddone", (e, data) => {
|
||||
let upload = data.result;
|
||||
|
||||
if (!this._xhr || !this._xhr._userCancelled) {
|
||||
const markdown = getUploadMarkdown(upload);
|
||||
cacheShortUploadUrl(upload.short_url, upload.url);
|
||||
this.appEvents.trigger('composer:replace-text', uploadPlaceholder, markdown);
|
||||
this._resetUpload(false);
|
||||
} else {
|
||||
this._resetUpload(true);
|
||||
}
|
||||
});
|
||||
|
||||
$element.on("fileuploadfail", (e, data) => {
|
||||
this._resetUpload(true);
|
||||
|
||||
@ -328,29 +446,12 @@ export default Ember.Component.extend({
|
||||
this._xhr = null;
|
||||
|
||||
if (!userCancelled) {
|
||||
displayErrorForUpload(data);
|
||||
}
|
||||
});
|
||||
|
||||
this.messageBus.subscribe("/uploads/composer", upload => {
|
||||
// replace upload placeholder
|
||||
if (upload && upload.url) {
|
||||
if (!this._xhr || !this._xhr._userCancelled) {
|
||||
const markdown = getUploadMarkdown(upload);
|
||||
cacheShortUploadUrl(upload.short_url, upload.url);
|
||||
this.appEvents.trigger('composer:replace-text', uploadPlaceholder, markdown);
|
||||
this._resetUpload(false);
|
||||
} else {
|
||||
this._resetUpload(true);
|
||||
}
|
||||
} else {
|
||||
this._resetUpload(true);
|
||||
displayErrorForUpload(upload);
|
||||
displayErrorForUpload(data.jqXHR.responseJSON);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.site.mobileView) {
|
||||
this.$(".mobile-file-upload").on("click.uploader", function () {
|
||||
$("#reply-control .mobile-file-upload").on("click.uploader", function () {
|
||||
// redirect the click on the hidden file input
|
||||
$("#mobile-uploader").click();
|
||||
});
|
||||
@ -360,29 +461,28 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
_optionsLocation() {
|
||||
// long term we want some smart positioning algorithm in popup-menu
|
||||
// the problem is that positioning in a fixed panel is a nightmare
|
||||
// cause offsetParent can end up returning a fixed element and then
|
||||
// using offset() is not going to work, so you end up needing special logic
|
||||
// especially since we allow for negative .top, provided there is room on screen
|
||||
const myPos = this.$().position();
|
||||
const buttonPos = this.$('.options').position();
|
||||
const composer = $("#reply-control");
|
||||
const composerOffset = composer.offset();
|
||||
const composerPosition = composer.position();
|
||||
|
||||
const popupHeight = $('#reply-control .popup-menu').height();
|
||||
const popupWidth = $('#reply-control .popup-menu').width();
|
||||
const buttonBarOffset = $('#reply-control .d-editor-button-bar').offset();
|
||||
const optionsButton = $('#reply-control .d-editor-button-bar .options');
|
||||
|
||||
var top = myPos.top + buttonPos.top - 15;
|
||||
var left = myPos.left + buttonPos.left - (popupWidth/2);
|
||||
const popupMenu = $("#reply-control .popup-menu");
|
||||
const popupWidth = popupMenu.outerWidth();
|
||||
const popupHeight = popupMenu.outerHeight();
|
||||
|
||||
const composerPos = $('#reply-control').position();
|
||||
const headerHeight = $(".d-header").outerHeight();
|
||||
|
||||
if (composerPos.top + top - popupHeight < 0) {
|
||||
top = top + popupHeight + this.$('.options').height() + 50;
|
||||
let left = optionsButton.offset().left - composerOffset.left;
|
||||
let top = buttonBarOffset.top - composerOffset.top - popupHeight + popupMenu.innerHeight();
|
||||
|
||||
if (top + composerPosition.top - headerHeight - popupHeight < 0) {
|
||||
top += popupHeight + optionsButton.outerHeight();
|
||||
}
|
||||
|
||||
var replyWidth = $('#reply-control').width();
|
||||
if (left + popupWidth > replyWidth) {
|
||||
left = replyWidth - popupWidth - 40;
|
||||
if (left + popupWidth > composer.width()) {
|
||||
left -= popupWidth - optionsButton.outerWidth();
|
||||
}
|
||||
|
||||
return { position: "absolute", left, top };
|
||||
@ -480,7 +580,7 @@ export default Ember.Component.extend({
|
||||
@on('willDestroyElement')
|
||||
_unbindUploadTarget() {
|
||||
this._validUploads = 0;
|
||||
this.$(".mobile-file-upload").off("click.uploader");
|
||||
$("#reply-control .mobile-file-upload").off("click.uploader");
|
||||
this.messageBus.unsubscribe("/uploads/composer");
|
||||
const $uploadTarget = this.$();
|
||||
try { $uploadTarget.fileupload("destroy"); }
|
||||
@ -491,14 +591,14 @@ export default Ember.Component.extend({
|
||||
@on('willDestroyElement')
|
||||
_composerClosed() {
|
||||
this.appEvents.trigger('composer:will-close');
|
||||
this.appEvents.off('composer:show-preview');
|
||||
this.appEvents.off('composer:hide-preview');
|
||||
Ember.run.next(() => {
|
||||
$('#main-outlet').css('padding-bottom', 0);
|
||||
// need to wait a bit for the "slide down" transition of the composer
|
||||
Ember.run.later(() => this.appEvents.trigger("composer:closed"), 400);
|
||||
});
|
||||
|
||||
this._teardownInputPreviewSync();
|
||||
|
||||
if (this.site.mobileView) {
|
||||
$(window).off('resize.composer-popup-menu');
|
||||
}
|
||||
@ -528,12 +628,12 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
showUploadModal(toolbarEvent) {
|
||||
this.sendAction('showUploadSelector', toolbarEvent);
|
||||
togglePreview() {
|
||||
this.sendAction('togglePreview');
|
||||
},
|
||||
|
||||
togglePreview() {
|
||||
this.toggleProperty('showPreview');
|
||||
showUploadModal(toolbarEvent) {
|
||||
this.sendAction('showUploadSelector', toolbarEvent);
|
||||
},
|
||||
|
||||
extraButtons(toolbar) {
|
||||
@ -605,18 +705,8 @@ export default Ember.Component.extend({
|
||||
Ember.run.debounce(this, this._loadOneboxes, $oneboxes, 450);
|
||||
}
|
||||
|
||||
// Short upload urls
|
||||
let $shortUploadUrls = $('img[data-orig-src]');
|
||||
|
||||
if ($shortUploadUrls.length > 0) {
|
||||
this._loadCachedShortUrls($shortUploadUrls);
|
||||
|
||||
$shortUploadUrls = $('img[data-orig-src]');
|
||||
if ($shortUploadUrls.length > 0) {
|
||||
// this is carefully batched so we can do an leading debounce (trigger right away)
|
||||
Ember.run.debounce(this, this._loadShortUrls, $shortUploadUrls, 450, true);
|
||||
}
|
||||
}
|
||||
// Short upload urls need resolution
|
||||
resolveAllShortUrls(ajax);
|
||||
|
||||
let inline = {};
|
||||
$('a.inline-onebox-loading', $preview).each(function(index, link) {
|
||||
@ -630,6 +720,7 @@ export default Ember.Component.extend({
|
||||
Ember.run.debounce(this, this._loadInlineOneboxes, inline, 450);
|
||||
}
|
||||
|
||||
this._syncScroll(this._syncEditorAndPreviewScroll, this.$('.d-editor-input'), $preview);
|
||||
this.trigger('previewRefreshed', $preview);
|
||||
this.sendAction('afterRefresh', $preview);
|
||||
},
|
||||
|
||||
@ -165,7 +165,6 @@ export default Ember.Component.extend({
|
||||
if (topicId) { args.topic_id = topicId; }
|
||||
if (postId) { args.post_id = postId; }
|
||||
|
||||
const queuedForTyping = this.get('queuedForTyping');
|
||||
composer.store.find('composer-message', args).then(messages => {
|
||||
if (this.isDestroying || this.isDestroyed) { return; }
|
||||
|
||||
@ -176,6 +175,7 @@ export default Ember.Component.extend({
|
||||
}
|
||||
|
||||
this.set('checkedMessages', true);
|
||||
const queuedForTyping = this.get('queuedForTyping');
|
||||
messages.forEach(msg => msg.wait_for_typing ? queuedForTyping.addObject(msg) : this.send('popup', msg));
|
||||
});
|
||||
}
|
||||
|
||||
@ -3,6 +3,14 @@ import computed from 'ember-addons/ember-computed-decorators';
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
|
||||
@computed('composeState')
|
||||
title(composeState) {
|
||||
if (composeState === "draft" || composeState === "saving") {
|
||||
return "composer.abandon";
|
||||
}
|
||||
return "composer.collapse";
|
||||
},
|
||||
|
||||
@computed('composeState')
|
||||
toggleIcon(composeState) {
|
||||
if (composeState === "draft" || composeState === "saving") {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: ['containerClass', 'condition:visible'],
|
||||
classNameBindings: [':loading-container', 'containerClass', 'condition:visible'],
|
||||
|
||||
@computed('size')
|
||||
containerClass(size) {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { cookAsync } from 'discourse/lib/text';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
const CookText = Ember.Component.extend({
|
||||
tagName: '',
|
||||
@ -6,7 +7,16 @@ const CookText = Ember.Component.extend({
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
cookAsync(this.get('rawText')).then(cooked => this.set('cooked', cooked));
|
||||
cookAsync(this.get('rawText')).then(
|
||||
cooked => {
|
||||
this.set('cooked', cooked);
|
||||
// no choice but to defer this cause
|
||||
// pretty text may only be loaded now
|
||||
Em.run.next(() =>
|
||||
window.requireModule('pretty-text/image-short-url').resolveAllShortUrls(ajax)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -438,7 +438,9 @@ export default Ember.Component.extend({
|
||||
}
|
||||
|
||||
if (operation !== OP.ADDED &&
|
||||
(l.slice(0, hlen) === hval && tlen === 0 || l.slice(-tlen) === tail)) {
|
||||
(l.slice(0, hlen) === hval && tlen === 0 ||
|
||||
(tail.length && l.slice(-tlen) === tail))) {
|
||||
|
||||
operation = OP.REMOVED;
|
||||
if (tlen === 0) {
|
||||
const result = l.slice(hlen);
|
||||
@ -500,6 +502,7 @@ export default Ember.Component.extend({
|
||||
tlen,
|
||||
opts
|
||||
);
|
||||
|
||||
this.set('value', `${pre}${contents}${post}`);
|
||||
if (lines.length === 1 && tlen > 0) {
|
||||
this._selectText(sel.start + hlen, sel.value.length);
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
|
||||
@computed('category')
|
||||
showCategoryNotifications(category) {
|
||||
return category && this.currentUser;
|
||||
},
|
||||
|
||||
@computed()
|
||||
categories() {
|
||||
return this.site.get('categoriesList');
|
||||
},
|
||||
|
||||
@computed('category.can_edit')
|
||||
showCategoryEdit: canEdit => canEdit,
|
||||
|
||||
@computed("filterMode", "category", 'noSubcategories')
|
||||
navItems(filterMode, category, noSubcategories) {
|
||||
// we don't want to show the period in the navigation bar since it's in a dropdown
|
||||
if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); }
|
||||
return Discourse.NavItem.buildList(category, { filterMode, noSubcategories });
|
||||
}
|
||||
});
|
||||
@ -2,56 +2,69 @@ import DiscourseURL from 'discourse/lib/url';
|
||||
import { buildCategoryPanel } from 'discourse/components/edit-category-panel';
|
||||
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
||||
import Category from 'discourse/models/category';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default buildCategoryPanel('general', {
|
||||
foregroundColors: ['FFFFFF', '000000'],
|
||||
canSelectParentCategory: Em.computed.not('category.isUncategorizedCategory'),
|
||||
|
||||
// background colors are available as a pipe-separated string
|
||||
backgroundColors: function() {
|
||||
const categories = Discourse.Category.list();
|
||||
@computed
|
||||
backgroundColors() {
|
||||
const categories = this.site.get('categoriesList');
|
||||
return this.siteSettings.category_colors.split("|").map(function(i) { return i.toUpperCase(); }).concat(
|
||||
categories.map(function(c) { return c.color.toUpperCase(); }) ).uniq();
|
||||
}.property(),
|
||||
},
|
||||
|
||||
usedBackgroundColors: function() {
|
||||
const categories = Discourse.Category.list();
|
||||
const category = this.get('category');
|
||||
@computed
|
||||
noCategoryStyle() {
|
||||
return this.siteSettings.category_style === 'none';
|
||||
},
|
||||
|
||||
@computed('category.id', 'category.color')
|
||||
usedBackgroundColors(categoryId, categoryColor) {
|
||||
const categories = this.site.get('categoriesList');
|
||||
|
||||
// If editing a category, don't include its color:
|
||||
return categories.map(function(c) {
|
||||
return (category.get('id') && category.get('color').toUpperCase() === c.color.toUpperCase()) ? null : c.color.toUpperCase();
|
||||
return (categoryId && categoryColor.toUpperCase() === c.color.toUpperCase()) ? null : c.color.toUpperCase();
|
||||
}, this).compact();
|
||||
}.property('category.id', 'category.color'),
|
||||
},
|
||||
|
||||
parentCategories: function() {
|
||||
return Discourse.Category.list().filter(function (c) {
|
||||
return !c.get('parentCategory');
|
||||
});
|
||||
}.property(),
|
||||
@computed
|
||||
parentCategories() {
|
||||
return this.site.get('categoriesList').filter(c => !c.get('parentCategory'));
|
||||
},
|
||||
|
||||
categoryBadgePreview: function() {
|
||||
@computed(
|
||||
'category.parent_category_id',
|
||||
'category.categoryName',
|
||||
'category.color',
|
||||
'category.text_color'
|
||||
)
|
||||
categoryBadgePreview(parentCategoryId, name, color, textColor) {
|
||||
const category = this.get('category');
|
||||
const c = Category.create({
|
||||
name: category.get('categoryName'),
|
||||
color: category.get('color'),
|
||||
text_color: category.get('text_color'),
|
||||
parent_category_id: parseInt(category.get('parent_category_id'),10),
|
||||
name,
|
||||
color,
|
||||
text_color: textColor,
|
||||
parent_category_id: parseInt(parentCategoryId),
|
||||
read_restricted: category.get('read_restricted')
|
||||
});
|
||||
return categoryBadgeHTML(c, {link: false});
|
||||
}.property('category.parent_category_id', 'category.categoryName', 'category.color', 'category.text_color'),
|
||||
|
||||
return categoryBadgeHTML(c, { link: false });
|
||||
},
|
||||
|
||||
// We can change the parent if there are no children
|
||||
subCategories: function() {
|
||||
if (Ember.isEmpty(this.get('category.id'))) { return null; }
|
||||
return Category.list().filterBy('parent_category_id', this.get('category.id'));
|
||||
}.property('category.id'),
|
||||
@computed('category.id')
|
||||
subCategories(categoryId) {
|
||||
if (Ember.isEmpty(categoryId)) { return null; }
|
||||
return Category.list().filterBy('parent_category_id', categoryId);
|
||||
},
|
||||
|
||||
showDescription: function() {
|
||||
return !this.get('category.isUncategorizedCategory') && this.get('category.id');
|
||||
}.property('category.isUncategorizedCategory', 'category.id'),
|
||||
@computed('category.isUncategorizedCategory', 'category.id')
|
||||
showDescription(isUncategorizedCategory, categoryId) {
|
||||
return !isUncategorizedCategory && categoryId;
|
||||
},
|
||||
|
||||
actions: {
|
||||
showCategoryTopic() {
|
||||
|
||||
@ -474,9 +474,8 @@ export default Ember.Component.extend({
|
||||
desktopModalePositioning();
|
||||
} else {
|
||||
let previewInputOffset = $(".d-editor-input").offset();
|
||||
let replyControlOffset = $("#reply-control").offset() || {left: 0};
|
||||
let left = previewInputOffset.left - replyControlOffset.left;
|
||||
desktopPositioning({left, bottom: $("#reply-control").height() - 48});
|
||||
let left = previewInputOffset.left;
|
||||
desktopPositioning({left, bottom: $("#reply-control").height() - 45});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,17 +2,28 @@ import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
expanded: null,
|
||||
_loading: false,
|
||||
|
||||
actions: {
|
||||
expandItem() {
|
||||
toggleItem() {
|
||||
if (this._loading) { return false; }
|
||||
const item = this.get('item');
|
||||
|
||||
if (this.get('expanded')) {
|
||||
this.set('expanded', false);
|
||||
item.set('expandedExcerpt', null);
|
||||
return;
|
||||
}
|
||||
|
||||
const topicId = item.get('topic_id');
|
||||
const postNumber = item.get('post_number');
|
||||
|
||||
this._loading = true;
|
||||
return ajax(`/posts/by_number/${topicId}/${postNumber}.json`).then(result => {
|
||||
item.set('truncated', false);
|
||||
item.set('excerpt', result.cooked);
|
||||
});
|
||||
this.set('expanded', true);
|
||||
item.set('expandedExcerpt', result.cooked);
|
||||
}).finally(() => this._loading = false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
|
||||
import {
|
||||
FORMAT,
|
||||
} from "select-box-kit/components/future-date-input-selector";
|
||||
} from "select-kit/components/future-date-input-selector";
|
||||
|
||||
import { PUBLISH_TO_CATEGORY_STATUS_TYPE } from 'discourse/controllers/edit-topic-timer';
|
||||
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'li'
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
|
||||
@computed('group')
|
||||
availableTabs(group) {
|
||||
return this.get('tabs').filter(t => {
|
||||
if (t.admin) {
|
||||
return this.currentUser ? this.currentUser.canManageGroup(group) : false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -5,7 +5,7 @@ import { renderedConnectorsFor } from 'discourse/lib/plugin-connectors';
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'ul',
|
||||
classNameBindings: [':nav', ':nav-pills'],
|
||||
id: 'navigation-bar',
|
||||
elementId: 'navigation-bar',
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
|
||||
@ -16,7 +16,18 @@ export default Ember.Component.extend(bufferedRender({
|
||||
|
||||
buildBuffer(buffer) {
|
||||
const content = this.get('content');
|
||||
buffer.push("<a href='" + content.get('href') + "'>");
|
||||
|
||||
let href = content.get('href');
|
||||
|
||||
// Include the category id if the option is present
|
||||
if (content.get('includeCategoryId')) {
|
||||
let categoryId = this.get('category.id');
|
||||
if (categoryId) {
|
||||
href += `?category_id=${categoryId}`;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.push(`<a href='${href}'>`);
|
||||
if (content.get('hasIcon')) {
|
||||
buffer.push("<span class='" + content.get('name') + "'></span>");
|
||||
}
|
||||
|
||||
@ -11,11 +11,8 @@ export default Ember.Component.extend({
|
||||
this.sendAction('hide');
|
||||
});
|
||||
|
||||
$('html').on(`mouseup.popup-menu-${this.get('elementId')}`, (e) => {
|
||||
const $target = $(e.target);
|
||||
if ($target.is("button") || this.$().has($target).length === 0) {
|
||||
this.sendAction('hide');
|
||||
}
|
||||
$('html').on(`mouseup.popup-menu-${this.get('elementId')}`, () => {
|
||||
this.sendAction('hide');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -40,9 +40,18 @@ export default Ember.Component.extend({
|
||||
}
|
||||
}
|
||||
|
||||
quoteState.selected(postId, selectedText());
|
||||
const _selectedText = selectedText();
|
||||
quoteState.selected(postId, _selectedText);
|
||||
this.set('visible', quoteState.buffer.length > 0);
|
||||
|
||||
// avoid hard loops in quote selection unconditionally
|
||||
// this can happen if you triple click text in firefox
|
||||
if (this._prevSelection === _selectedText) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._prevSelection = _selectedText;
|
||||
|
||||
// on Desktop, shows the button at the beginning of the selection
|
||||
// on Mobile, shows the button at the end of the selection
|
||||
const isMobileDevice = this.site.isMobileDevice;
|
||||
@ -101,12 +110,14 @@ export default Ember.Component.extend({
|
||||
const onSelectionChanged = _.debounce(() => this._selectionChanged(), wait);
|
||||
|
||||
$(document).on("mousedown.quote-button", e => {
|
||||
this._prevSelection = null;
|
||||
this._isMouseDown = true;
|
||||
this._reselected = false;
|
||||
if ($(e.target).closest('.quote-button, .create, .share, .reply-new').length === 0) {
|
||||
this._hideButton();
|
||||
}
|
||||
}).on("mouseup.quote-button", () => {
|
||||
this._prevSelection = null;
|
||||
this._isMouseDown = false;
|
||||
onSelectionChanged();
|
||||
}).on("selectionchange.quote-button", () => {
|
||||
|
||||
@ -4,13 +4,13 @@ import { cloak, uncloak } from 'discourse/widgets/post-stream';
|
||||
import { isWorkaroundActive } from 'discourse/lib/safari-hacks';
|
||||
import offsetCalculator from 'discourse/lib/offset-calculator';
|
||||
|
||||
function findTopView($posts, viewportTop, min, max) {
|
||||
function findTopView($posts, viewportTop, postsWrapperTop, min, max) {
|
||||
if (max < min) { return min; }
|
||||
|
||||
while (max > min) {
|
||||
const mid = Math.floor((min + max) / 2);
|
||||
const $post = $($posts[mid]);
|
||||
const viewBottom = $post.position().top + $post.height();
|
||||
const viewBottom = ($post.offset().top - postsWrapperTop) + $post.height();
|
||||
|
||||
if (viewBottom > viewportTop) {
|
||||
max = mid-1;
|
||||
@ -63,6 +63,10 @@ export default MountWidget.extend({
|
||||
if (this.isDestroyed || this.isDestroying) { return; }
|
||||
if (isWorkaroundActive()) { return; }
|
||||
|
||||
// We use this because watching videos fullscreen in Chrome was super buggy
|
||||
// otherwise. Thanks to arrendek from q23 for the technique.
|
||||
if (document.elementFromPoint(0, 0).tagName.toUpperCase() === "IFRAME") { return; }
|
||||
|
||||
const $w = $(window);
|
||||
const windowHeight = window.innerHeight ? window.innerHeight : $w.height();
|
||||
const slack = Math.round(windowHeight * 5);
|
||||
@ -71,9 +75,10 @@ export default MountWidget.extend({
|
||||
|
||||
const windowTop = $w.scrollTop();
|
||||
|
||||
const postsWrapperTop = $('.posts-wrapper').offset().top;
|
||||
const $posts = this.$('.onscreen-post, .cloaked-post');
|
||||
const viewportTop = windowTop - slack;
|
||||
const topView = findTopView($posts, viewportTop, 0, $posts.length-1);
|
||||
const topView = findTopView($posts, viewportTop, postsWrapperTop, 0, $posts.length-1);
|
||||
|
||||
let windowBottom = windowTop + windowHeight;
|
||||
let viewportBottom = windowBottom + slack;
|
||||
|
||||
@ -185,18 +185,18 @@ export default Em.Component.extend({
|
||||
const userInput = Discourse.Category.findBySlug(subcategories[1], subcategories[0]);
|
||||
if ((!existingInput && userInput)
|
||||
|| (existingInput && userInput && existingInput.id !== userInput.id))
|
||||
this.set('searchedTerms.category', [userInput]);
|
||||
this.set('searchedTerms.category', userInput);
|
||||
} else
|
||||
if (isNaN(subcategories)) {
|
||||
const userInput = Discourse.Category.findSingleBySlug(subcategories[0]);
|
||||
if ((!existingInput && userInput)
|
||||
|| (existingInput && userInput && existingInput.id !== userInput.id))
|
||||
this.set('searchedTerms.category', [userInput]);
|
||||
this.set('searchedTerms.category', userInput);
|
||||
} else {
|
||||
const userInput = Discourse.Category.findById(subcategories[0]);
|
||||
if ((!existingInput && userInput)
|
||||
|| (existingInput && userInput && existingInput.id !== userInput.id))
|
||||
this.set('searchedTerms.category', [userInput]);
|
||||
this.set('searchedTerms.category', userInput);
|
||||
}
|
||||
} else
|
||||
this.set('searchedTerms.category', '');
|
||||
@ -303,11 +303,11 @@ export default Em.Component.extend({
|
||||
|
||||
const slugCategoryMatches = (match.length !== 0) ? match[0].match(REGEXP_CATEGORY_SLUG) : null;
|
||||
const idCategoryMatches = (match.length !== 0) ? match[0].match(REGEXP_CATEGORY_ID) : null;
|
||||
if (categoryFilter && categoryFilter[0]) {
|
||||
const id = categoryFilter[0].id;
|
||||
const slug = categoryFilter[0].slug;
|
||||
if (categoryFilter[0].parentCategory) {
|
||||
const parentSlug = categoryFilter[0].parentCategory.slug;
|
||||
if (categoryFilter) {
|
||||
const id = categoryFilter.id;
|
||||
const slug = categoryFilter.slug;
|
||||
if (categoryFilter.parentCategory) {
|
||||
const parentSlug = categoryFilter.parentCategory.slug;
|
||||
if (slugCategoryMatches)
|
||||
searchTerm = searchTerm.replace(slugCategoryMatches[0], `#${parentSlug}:${slug}`);
|
||||
else if (idCategoryMatches)
|
||||
|
||||
@ -12,6 +12,7 @@ export default MountWidget.extend(Docking, {
|
||||
buildArgs() {
|
||||
let attrs = {
|
||||
topic: this.get('topic'),
|
||||
notificationLevel: this.get('notificationLevel'),
|
||||
topicTrackingState: this.topicTrackingState,
|
||||
enteredIndex: this.get('enteredIndex'),
|
||||
dockAt: this.dockAt,
|
||||
|
||||
@ -6,12 +6,14 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decor
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import User from 'discourse/models/user';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
import { durationTiny } from 'discourse/lib/formatter';
|
||||
import CanCheckEmails from 'discourse/mixins/can-check-emails';
|
||||
|
||||
const clickOutsideEventName = "mousedown.outside-user-card";
|
||||
const clickDataExpand = "click.discourse-user-card";
|
||||
const clickMention = "click.discourse-user-mention";
|
||||
|
||||
export default Ember.Component.extend(CleansUp, {
|
||||
export default Ember.Component.extend(CleansUp, CanCheckEmails, {
|
||||
elementId: 'user-card',
|
||||
classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'user.card_background::no-bg'],
|
||||
allowBackgrounds: setting('allow_profile_backgrounds'),
|
||||
@ -29,6 +31,7 @@ export default Ember.Component.extend(CleansUp, {
|
||||
showDelete: Ember.computed.and("viewingAdmin", "showName", "user.canBeDeleted"),
|
||||
linkWebsite: Ember.computed.not('user.isBasic'),
|
||||
hasLocationOrWebsite: Ember.computed.or('user.location', 'user.website_name'),
|
||||
showCheckEmail: Ember.computed.and('user.staged', 'canCheckEmails'),
|
||||
|
||||
visible: false,
|
||||
user: null,
|
||||
@ -87,6 +90,25 @@ export default Ember.Component.extend(CleansUp, {
|
||||
$this.css('background-image', bg);
|
||||
},
|
||||
|
||||
@computed('user.time_read', 'user.recent_time_read')
|
||||
showRecentTimeRead(timeRead, recentTimeRead) {
|
||||
return timeRead !== recentTimeRead && recentTimeRead !== 0;
|
||||
},
|
||||
|
||||
@computed('user.recent_time_read')
|
||||
recentTimeRead(recentTimeReadSeconds) {
|
||||
return durationTiny(recentTimeReadSeconds);
|
||||
},
|
||||
|
||||
@computed('showRecentTimeRead', 'user.time_read', 'recentTimeRead')
|
||||
timeReadTooltip(showRecent, timeRead, recentTimeRead) {
|
||||
if (showRecent) {
|
||||
return I18n.t('time_read_recently_tooltip', {time_read: durationTiny(timeRead), recent_time_read: recentTimeRead});
|
||||
} else {
|
||||
return I18n.t('time_read_tooltip', {time_read: durationTiny(timeRead)});
|
||||
}
|
||||
},
|
||||
|
||||
_show(username, $target) {
|
||||
// No user card for anon
|
||||
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
|
||||
@ -271,6 +293,10 @@ export default Ember.Component.extend(CleansUp, {
|
||||
showUser() {
|
||||
this.sendAction('showUser', this.get('user'));
|
||||
this._close();
|
||||
},
|
||||
|
||||
checkEmail(user) {
|
||||
user.checkEmail();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -42,7 +42,7 @@ export default TextField.extend({
|
||||
allowAny: this.get('allowAny'),
|
||||
updateData: (opts && opts.updateData) ? opts.updateData : false,
|
||||
|
||||
dataSource: function(term) {
|
||||
dataSource(term) {
|
||||
const termRegex = Discourse.User.currentProp('can_send_private_email_messages') ?
|
||||
/[^a-zA-Z0-9_\-\.@\+]/ : /[^a-zA-Z0-9_\-\.]/;
|
||||
|
||||
@ -60,7 +60,7 @@ export default TextField.extend({
|
||||
return results;
|
||||
},
|
||||
|
||||
transformComplete: function(v) {
|
||||
transformComplete(v) {
|
||||
if (v.username || v.name) {
|
||||
if (!v.username) { groups.push(v.name); }
|
||||
return v.username || v.name;
|
||||
@ -72,7 +72,7 @@ export default TextField.extend({
|
||||
}
|
||||
},
|
||||
|
||||
onChangeItems: function(items) {
|
||||
onChangeItems(items) {
|
||||
var hasGroups = false;
|
||||
items = items.map(function(i) {
|
||||
if (groups.indexOf(i) > -1) { hasGroups = true; }
|
||||
@ -85,7 +85,7 @@ export default TextField.extend({
|
||||
if (self.get('onChangeCallback')) self.sendAction('onChangeCallback');
|
||||
},
|
||||
|
||||
reverseTransform: function(i) {
|
||||
reverseTransform(i) {
|
||||
return { username: i };
|
||||
}
|
||||
|
||||
|
||||
@ -36,10 +36,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
refreshGravatar() {
|
||||
this.set("gravatarRefreshDisabled", true);
|
||||
return ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" })
|
||||
.then(result => this.setProperties({
|
||||
gravatar_avatar_template: result.gravatar_avatar_template,
|
||||
gravatar_avatar_upload_id: result.gravatar_upload_id,
|
||||
}))
|
||||
.then(result => {
|
||||
if (!result.gravatar_avatar_upload_id) {
|
||||
this.set("gravatarFailed", true);
|
||||
} else {
|
||||
this.setProperties({
|
||||
gravatarFailed: false,
|
||||
gravatar_avatar_template: result.gravatar_avatar_template,
|
||||
gravatar_avatar_upload_id: result.gravatar_upload_id,
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => this.set("gravatarRefreshDisabled", false));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import DiscourseURL from 'discourse/lib/url';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import Draft from 'discourse/models/draft';
|
||||
import Composer from 'discourse/models/composer';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators';
|
||||
import InputValidation from 'discourse/models/input-validation';
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
@ -68,7 +68,28 @@ export default Ember.Controller.extend({
|
||||
isUploading: false,
|
||||
topic: null,
|
||||
linkLookup: null,
|
||||
showPreview: true,
|
||||
forcePreview: Ember.computed.and('site.mobileView', 'showPreview'),
|
||||
whisperOrUnlistTopic: Ember.computed.or('model.whisper', 'model.unlistTopic'),
|
||||
categories: Ember.computed.alias('site.categoriesList'),
|
||||
|
||||
@on('init')
|
||||
_setupPreview() {
|
||||
const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true'));
|
||||
this.set('showPreview', val === 'true');
|
||||
},
|
||||
|
||||
@computed('showPreview')
|
||||
toggleText: function(showPreview) {
|
||||
return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview');
|
||||
},
|
||||
|
||||
@observes('showPreview')
|
||||
showPreviewChanged() {
|
||||
if (!this.site.mobileView) {
|
||||
this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
|
||||
}
|
||||
},
|
||||
|
||||
@computed('model.replyingToTopic', 'model.creatingPrivateMessage', 'model.targetUsernames')
|
||||
focusTarget(replyingToTopic, creatingPM, usernames) {
|
||||
@ -205,6 +226,10 @@ export default Ember.Controller.extend({
|
||||
|
||||
actions: {
|
||||
|
||||
togglePreview() {
|
||||
this.toggleProperty('showPreview');
|
||||
},
|
||||
|
||||
typed() {
|
||||
this.checkReplyLength();
|
||||
this.get('model').typing();
|
||||
@ -278,20 +303,18 @@ export default Ember.Controller.extend({
|
||||
// Toggle the reply view
|
||||
toggle() {
|
||||
this.closeAutocomplete();
|
||||
if (this.get('model.composeState') === Composer.OPEN) {
|
||||
if (Ember.isEmpty(this.get('model.reply')) && Ember.isEmpty(this.get('model.title'))) {
|
||||
this.close();
|
||||
} else {
|
||||
this.shrink();
|
||||
}
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
togglePreview() {
|
||||
this.get('model').togglePreview();
|
||||
if (Ember.isEmpty(this.get('model.reply')) && Ember.isEmpty(this.get('model.title'))) {
|
||||
this.close();
|
||||
} else {
|
||||
if (this.get('model.composeState') === Composer.OPEN) {
|
||||
this.shrink();
|
||||
} else {
|
||||
this.cancelComposer();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// Import a quote from the post
|
||||
@ -367,8 +390,9 @@ export default Ember.Controller.extend({
|
||||
const body = I18n.t('composer.group_mentioned', {
|
||||
group: "@" + group.name,
|
||||
count: group.user_count,
|
||||
group_link: Discourse.getURL(`/group/${group.name}/members`)
|
||||
group_link: Discourse.getURL(`/groups/${group.name}/members`)
|
||||
});
|
||||
|
||||
this.appEvents.trigger('composer-messages:create', {
|
||||
extraClass: 'custom-body',
|
||||
templateName: 'custom-body',
|
||||
@ -396,10 +420,6 @@ export default Ember.Controller.extend({
|
||||
|
||||
},
|
||||
|
||||
categories: function() {
|
||||
return Discourse.Category.list();
|
||||
}.property(),
|
||||
|
||||
disableSubmit: Ember.computed.or("model.loading", "isUploading"),
|
||||
|
||||
save(force) {
|
||||
@ -654,7 +674,7 @@ export default Ember.Controller.extend({
|
||||
if (!splitCategory[1]) {
|
||||
category = this.site.get('categories').findBy('nameLower', splitCategory[0].toLowerCase());
|
||||
} else {
|
||||
const categories = Discourse.Category.list();
|
||||
const categories = this.site.get('categories');
|
||||
const mainCategory = categories.findBy('nameLower', splitCategory[0].toLowerCase());
|
||||
category = categories.find(function(item) {
|
||||
return item && item.get('nameLower') === splitCategory[1].toLowerCase() && item.get('parent_category_id') === mainCategory.id;
|
||||
@ -720,7 +740,7 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
shrink() {
|
||||
if (this.get('model.replyDirty')) {
|
||||
if (this.get('model.replyDirty') || (this.get('model.canEditTitle') && this.get('model.titleDirty'))) {
|
||||
this.collapse();
|
||||
} else {
|
||||
this.close();
|
||||
|
||||
@ -65,7 +65,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
saveCategory() {
|
||||
const self = this,
|
||||
model = this.get('model'),
|
||||
parentCategory = Discourse.Category.list().findBy('id', parseInt(model.get('parent_category_id'), 10));
|
||||
parentCategory = this.site.get('categories').findBy('id', parseInt(model.get('parent_category_id'), 10));
|
||||
|
||||
this.set('saving', true);
|
||||
model.set('parentCategory', parentCategory);
|
||||
|
||||
@ -104,11 +104,13 @@ export default Ember.Controller.extend({
|
||||
cleanTerm(term) {
|
||||
if (term) {
|
||||
SortOrders.forEach(order => {
|
||||
let matches = term.match(new RegExp(`${order.term}\\b`));
|
||||
if (matches) {
|
||||
this.set('sortOrder', order.id);
|
||||
term = term.replace(new RegExp(`${order.term}\\b`, 'g'), "");
|
||||
term = term.trim();
|
||||
if (order.term) {
|
||||
let matches = term.match(new RegExp(`${order.term}\\b`));
|
||||
if (matches) {
|
||||
this.set('sortOrder', order.id);
|
||||
term = term.replace(new RegExp(`${order.term}\\b`, 'g'), "");
|
||||
term = term.trim();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -159,9 +161,9 @@ export default Ember.Controller.extend({
|
||||
return this.currentUser && this.currentUser.staff && hasResults;
|
||||
},
|
||||
|
||||
@computed('expanded')
|
||||
canCreateTopic(expanded) {
|
||||
return this.currentUser && !this.site.mobileView && !expanded;
|
||||
@computed('expanded', 'model.grouped_search_result.can_create_topic')
|
||||
canCreateTopic(expanded, userCanCreateTopic) {
|
||||
return this.currentUser && userCanCreateTopic && !this.site.mobileView && !expanded;
|
||||
},
|
||||
|
||||
@computed('expanded')
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { fmt } from 'discourse/lib/computed';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
group: Ember.inject.controller(),
|
||||
groupActivity: Ember.inject.controller(),
|
||||
application: Ember.inject.controller(),
|
||||
canLoadMore: true,
|
||||
loading: false,
|
||||
emptyText: fmt('type', 'groups.empty.%@'),
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
if (!this.get('canLoadMore')) { return; }
|
||||
if (this.get('loading')) { return; }
|
||||
this.set('loading', true);
|
||||
const posts = this.get('model');
|
||||
@ -14,12 +19,23 @@ export default Ember.Controller.extend({
|
||||
const beforePostId = posts[posts.length-1].get('id');
|
||||
const group = this.get('group.model');
|
||||
|
||||
const opts = { beforePostId, type: this.get('type') };
|
||||
let categoryId = this.get('groupActivity.category_id');
|
||||
const opts = { beforePostId, type: this.get('type'), categoryId };
|
||||
|
||||
group.findPosts(opts).then(newPosts => {
|
||||
posts.addObjects(newPosts);
|
||||
if(newPosts.length === 0) {
|
||||
this.set('canLoadMore', false);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@observes('canLoadMore')
|
||||
_showFooter() {
|
||||
this.set("application.showFooter", !this.get("canLoadMore"));
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
application: Ember.inject.controller(),
|
||||
queryParams: ['category_id'],
|
||||
|
||||
@computed('model.is_group_user')
|
||||
showGroupMessages(isGroupUser) {
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
var Tab = Em.Object.extend({
|
||||
@computed('name')
|
||||
location(name) {
|
||||
return 'group.' + name;
|
||||
},
|
||||
|
||||
@computed('name', 'i18nKey')
|
||||
message(name, i18nKey) {
|
||||
return I18n.t(`groups.${i18nKey || name}`);
|
||||
const Tab = Ember.Object.extend({
|
||||
init() {
|
||||
this._super();
|
||||
let name = this.get('name');
|
||||
this.set('route', this.get('route') || `group.` + name);
|
||||
this.set('message', I18n.t(`groups.${this.get('i18nKey') || name}`));
|
||||
}
|
||||
});
|
||||
|
||||
@ -18,13 +15,13 @@ export default Ember.Controller.extend({
|
||||
showing: 'members',
|
||||
|
||||
tabs: [
|
||||
Tab.create({ name: 'members', 'location': 'group.index', icon: 'users' }),
|
||||
Tab.create({ name: 'members', route: 'group.index', icon: 'users' }),
|
||||
Tab.create({ name: 'activity' }),
|
||||
Tab.create({
|
||||
name: 'edit', i18nKey: 'edit.title', icon: 'pencil', requiresGroupAdmin: true
|
||||
name: 'edit', i18nKey: 'edit.title', icon: 'pencil', admin: true
|
||||
}),
|
||||
Tab.create({
|
||||
name: 'logs', i18nKey: 'logs.title', icon: 'list-alt', requiresGroupAdmin: true
|
||||
name: 'logs', i18nKey: 'logs.title', icon: 'list-alt', admin: true
|
||||
})
|
||||
],
|
||||
|
||||
@ -58,21 +55,6 @@ export default Ember.Controller.extend({
|
||||
this.get('tabs')[0].set('count', this.get('model.user_count'));
|
||||
},
|
||||
|
||||
@computed('model.is_group_owner', 'model.automatic')
|
||||
getTabs() {
|
||||
return this.get('tabs').filter(t => {
|
||||
let canSee = true;
|
||||
|
||||
if (this.currentUser && t.requiresGroupAdmin) {
|
||||
canSee = this.currentUser.canManageGroup(this.get('model'));
|
||||
} else if (t.requiresGroupAdmin) {
|
||||
canSee = false;
|
||||
}
|
||||
|
||||
return canSee;
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
messageGroup() {
|
||||
this.send('createNewMessageViaParams', this.get('model.name'));
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import NavigationDefaultController from 'discourse/controllers/navigation/default';
|
||||
|
||||
export default NavigationDefaultController.extend({
|
||||
showingParentCategory: Em.computed.none('category.parentCategory'),
|
||||
showingSubcategoryList: Em.computed.and('category.show_subcategory_list', 'showingParentCategory'),
|
||||
|
||||
@computed("showingSubcategoryList", "category", "noSubcategories")
|
||||
navItems(showingSubcategoryList, category, noSubcategories) {
|
||||
return Discourse.NavItem.buildList(category, { noSubcategories });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,19 +1,4 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
discovery: Ember.inject.controller(),
|
||||
discoveryTopics: Ember.inject.controller('discovery/topics'),
|
||||
|
||||
@computed()
|
||||
categories() {
|
||||
return Discourse.Category.list();
|
||||
},
|
||||
|
||||
@computed("filterMode")
|
||||
navItems(filterMode) {
|
||||
// we don't want to show the period in the navigation bar since it's in a dropdown
|
||||
if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); }
|
||||
return Discourse.NavItem.buildList(null, { filterMode });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -7,6 +7,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'),
|
||||
passwordRequired: true,
|
||||
errorMessage: null,
|
||||
successMessage: null,
|
||||
|
||||
@ -1,28 +1,9 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import BadgeSelectController from "discourse/mixins/badge-select-controller";
|
||||
|
||||
export default Ember.Controller.extend(BadgeSelectController, {
|
||||
|
||||
filteredList: function() {
|
||||
return this.get('model').filterBy('badge.allow_title', true);
|
||||
}.property('model'),
|
||||
}.property('model')
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.setProperties({ saved: false, saving: true });
|
||||
|
||||
ajax(this.get('user.path') + "/preferences/badge_title", {
|
||||
type: "PUT",
|
||||
data: { user_badge_id: this.get('selectedUserBadgeId') }
|
||||
}).then(() => {
|
||||
this.setProperties({
|
||||
saved: true,
|
||||
saving: false,
|
||||
"user.title": this.get('selectedUserBadge.badge.name')
|
||||
});
|
||||
}, () => {
|
||||
bootbox.alert(I18n.t('generic_error'));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
|
||||
import { setDefaultHomepage } from "discourse/lib/utilities";
|
||||
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
|
||||
import { currentThemeKey, listThemes, previewTheme, setLocalTheme } from 'discourse/lib/theme-selector';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
const USER_HOMES = { 1: "latest", 2: "categories", 3: "unread", 4: "new", 5: "top" };
|
||||
|
||||
export default Ember.Controller.extend(PreferencesTabController, {
|
||||
|
||||
@computed("makeThemeDefault")
|
||||
@ -14,6 +17,8 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
'enable_quoting',
|
||||
'disable_jump_reply',
|
||||
'automatically_unpin_topics',
|
||||
'allow_private_messages',
|
||||
'homepage_id',
|
||||
];
|
||||
|
||||
if (makeDefault) {
|
||||
@ -51,6 +56,19 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
previewTheme(key);
|
||||
},
|
||||
|
||||
homeChanged() {
|
||||
const siteHome = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
|
||||
const userHome = USER_HOMES[this.get('model.user_option.homepage_id')];
|
||||
setDefaultHomepage(userHome || siteHome);
|
||||
},
|
||||
|
||||
@computed()
|
||||
userSelectableHome() {
|
||||
return _.map(USER_HOMES, (name, num) => {
|
||||
return {name: I18n.t('filters.' + name + '.title'), value: Number(num)};
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set('saved', false);
|
||||
@ -66,6 +84,8 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
setLocalTheme(this.get('themeKey'), this.get('model.user_option.theme_key_seq'));
|
||||
}
|
||||
|
||||
this.homeChanged();
|
||||
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,6 +58,8 @@ export default Ember.Controller.extend(BulkTopicSelection, {
|
||||
max_posts: null,
|
||||
q: null,
|
||||
|
||||
categories: Ember.computed.alias('site.categoriesList'),
|
||||
|
||||
queryParams: ['order', 'ascending', 'status', 'state', 'search', 'max_posts', 'q'],
|
||||
|
||||
navItems: function() {
|
||||
@ -68,10 +70,6 @@ export default Ember.Controller.extend(BulkTopicSelection, {
|
||||
return Discourse.SiteSettings.show_filter_by_tag;
|
||||
}.property('category'),
|
||||
|
||||
categories: function() {
|
||||
return Discourse.Category.list();
|
||||
}.property(),
|
||||
|
||||
showAdminControls: function() {
|
||||
return !this.get('additionalTags') && this.get('canAdminTag') && !this.get('category');
|
||||
}.property('additionalTags', 'canAdminTag', 'category'),
|
||||
|
||||
@ -115,7 +115,6 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
showChangeCategory() {
|
||||
this.send('changeBulkTemplate', 'modal/bulk-change-category');
|
||||
this.set('modal.modalClass', 'topic-bulk-actions-modal full');
|
||||
},
|
||||
|
||||
showNotificationLevel() {
|
||||
|
||||
@ -12,6 +12,7 @@ import debounce from 'discourse/lib/debounce';
|
||||
import isElementInViewport from "discourse/lib/is-element-in-viewport";
|
||||
import QuoteState from 'discourse/lib/quote-state';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
import { extractLinkMeta } from 'discourse/lib/render-topic-featured-link';
|
||||
|
||||
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
composer: Ember.inject.controller(),
|
||||
@ -32,6 +33,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
username_filters: null,
|
||||
filter: null,
|
||||
quoteState: null,
|
||||
canRemoveTopicFeaturedLink: Ember.computed.and('canEditTopicFeaturedLink', 'buffered.featured_link'),
|
||||
|
||||
updateQueryParams() {
|
||||
const postStream = this.get('model.postStream');
|
||||
@ -99,6 +101,12 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
return categoryIds === undefined || !categoryIds.length || categoryIds.indexOf(categoryId) !== -1;
|
||||
},
|
||||
|
||||
@computed('model')
|
||||
featuredLinkDomain(topic) {
|
||||
const meta = extractLinkMeta(topic);
|
||||
return meta.domain;
|
||||
},
|
||||
|
||||
@computed('model.isPrivateMessage')
|
||||
canEditTags(isPrivateMessage) {
|
||||
return !isPrivateMessage && this.site.get('can_tag_topics');
|
||||
@ -123,9 +131,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
const composer = this.get('composer');
|
||||
const viewOpen = composer.get('model.viewOpen');
|
||||
|
||||
const quotedText = Quote.build(post, buffer);
|
||||
|
||||
// If we can't create a post, delegate to reply as new topic
|
||||
if ((!viewOpen) && (!this.get('model.details.can_create_post'))) {
|
||||
this.send('replyAsNewTopic', post);
|
||||
this.send('replyAsNewTopic', post, quotedText);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -146,7 +156,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
composerOpts.post = composerPost;
|
||||
}
|
||||
|
||||
const quotedText = Quote.build(post, buffer);
|
||||
composerOpts.quote = quotedText;
|
||||
if (composer.get('model.viewOpen')) {
|
||||
this.appEvents.trigger('composer:insert-block', quotedText);
|
||||
@ -615,11 +624,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
}
|
||||
},
|
||||
|
||||
replyAsNewTopic(post) {
|
||||
replyAsNewTopic(post, quotedText) {
|
||||
const composerController = this.get('composer');
|
||||
|
||||
const { quoteState } = this;
|
||||
const quotedText = Quote.build(post, quoteState.buffer);
|
||||
quotedText = quotedText || Quote.build(post, quoteState.buffer);
|
||||
quoteState.clear();
|
||||
|
||||
var options;
|
||||
@ -694,6 +703,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
|
||||
convertToPrivateMessage() {
|
||||
this.get('content').convertTopic("private");
|
||||
},
|
||||
|
||||
removeFeaturedLink() {
|
||||
this.set('buffered.featured_link', null);
|
||||
}
|
||||
},
|
||||
|
||||
@ -752,9 +765,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
return selectedPostsUsername !== undefined;
|
||||
},
|
||||
|
||||
categories: function() {
|
||||
return Discourse.Category.list();
|
||||
}.property(),
|
||||
categories: Ember.computed.alias('site.categoriesList'),
|
||||
|
||||
canSelectAll: Em.computed.not('allPostsSelected'),
|
||||
|
||||
|
||||
@ -3,11 +3,9 @@ import { exportUserArchive } from 'discourse/lib/export-csv';
|
||||
export default Ember.Controller.extend({
|
||||
application: Ember.inject.controller(),
|
||||
user: Ember.inject.controller(),
|
||||
|
||||
userActionType: null,
|
||||
currentPath: Ember.computed.alias('application.currentPath'),
|
||||
viewingSelf: Ember.computed.alias("user.viewingSelf"),
|
||||
showBookmarks: Ember.computed.alias("user.showBookmarks"),
|
||||
|
||||
canDownloadPosts: Ember.computed.alias('user.viewingSelf'),
|
||||
|
||||
_showFooter: function() {
|
||||
var showFooter;
|
||||
@ -26,11 +24,7 @@ export default Ember.Controller.extend({
|
||||
I18n.t("user.download_archive.confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
exportUserArchive();
|
||||
}
|
||||
}
|
||||
confirmed => confirmed ? exportUserArchive() : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { durationTiny } from 'discourse/lib/formatter';
|
||||
|
||||
// should be kept in sync with 'UserSummary::MAX_BADGES'
|
||||
const MAX_BADGES = 6;
|
||||
@ -9,4 +10,19 @@ export default Ember.Controller.extend({
|
||||
|
||||
@computed("model.badges.length")
|
||||
moreBadges(badgesLength) { return badgesLength >= MAX_BADGES; },
|
||||
|
||||
@computed('model.time_read')
|
||||
timeRead(timeReadSeconds) {
|
||||
return durationTiny(timeReadSeconds);
|
||||
},
|
||||
|
||||
@computed('model.time_read', 'model.recent_time_read')
|
||||
showRecentTimeRead(timeRead, recentTimeRead) {
|
||||
return timeRead !== recentTimeRead && recentTimeRead !== 0;
|
||||
},
|
||||
|
||||
@computed('model.recent_time_read')
|
||||
recentTimeRead(recentTimeReadSeconds) {
|
||||
return recentTimeReadSeconds > 0 ? durationTiny(recentTimeReadSeconds) : null;
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import { htmlHelper } from 'discourse-common/lib/helpers';
|
||||
import { avatarImg } from 'discourse/lib/utilities';
|
||||
|
||||
export default htmlHelper((avatarTemplate, size) => avatarImg({ size, avatarTemplate }));
|
||||
export default htmlHelper((avatarTemplate, size) => {
|
||||
if (Ember.isEmpty(avatarTemplate)) {
|
||||
return "<div class='avatar-placeholder'></div>";
|
||||
} else {
|
||||
return avatarImg({ size, avatarTemplate });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
|
||||
import { autoUpdatingRelativeAge, durationTiny } from 'discourse/lib/formatter';
|
||||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||
|
||||
registerUnbound('format-age', function(dt) {
|
||||
dt = new Date(dt);
|
||||
return new Handlebars.SafeString(autoUpdatingRelativeAge(dt));
|
||||
});
|
||||
|
||||
registerUnbound('format-duration', function(seconds) {
|
||||
return new Handlebars.SafeString(durationTiny(seconds));
|
||||
});
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||
import { formatUsername } from 'discourse/lib/utilities';
|
||||
|
||||
export default registerUnbound('format-username', formatUsername);
|
||||
@ -1,5 +1,5 @@
|
||||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||
import { avatarImg } from 'discourse/lib/utilities';
|
||||
import { avatarImg, formatUsername } from 'discourse/lib/utilities';
|
||||
|
||||
function renderAvatar(user, options) {
|
||||
options = options || {};
|
||||
@ -11,6 +11,8 @@ function renderAvatar(user, options) {
|
||||
|
||||
if (!username || !avatarTemplate) { return ''; }
|
||||
|
||||
let formattedUsername = formatUsername(username);
|
||||
|
||||
let title = options.title;
|
||||
if (!title && !options.ignoreTitle) {
|
||||
// first try to get a title
|
||||
@ -22,7 +24,7 @@ function renderAvatar(user, options) {
|
||||
// if a description has been provided
|
||||
if (description && description.length > 0) {
|
||||
// preprend the username before the description
|
||||
title = username + " - " + description;
|
||||
title = formattedUsername + " - " + description;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,7 +32,7 @@ function renderAvatar(user, options) {
|
||||
return avatarImg({
|
||||
size: options.imageSize,
|
||||
extraClasses: Em.get(user, 'extras') || options.extraClasses,
|
||||
title: title || username,
|
||||
title: title || formattedUsername,
|
||||
avatarTemplate: avatarTemplate
|
||||
});
|
||||
} else {
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
// Android Chrome App Banner requires at least **one** service worker to be instantiate and https.
|
||||
// After Discourse starts to use service workers for other stuff (like mobile notification, offline mode, or ember)
|
||||
// we can ditch this.
|
||||
|
||||
export default {
|
||||
name: 'android-app-banner-service-worker',
|
||||
|
||||
initialize(container) {
|
||||
const caps = container.lookup('capabilities:main');
|
||||
const isSecure = document.location.protocol === 'https:';
|
||||
|
||||
if (isSecure && caps.isAndroid && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register(Discourse.BaseUri + '/service-worker.js', {scope: './'});
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,12 @@
|
||||
export default {
|
||||
name: 'register-service-worker',
|
||||
|
||||
initialize() {
|
||||
const isSecure = (document.location.protocol === 'https:') ||
|
||||
(location.hostname === "localhost");
|
||||
|
||||
if (isSecure && ('serviceWorker' in navigator)) {
|
||||
navigator.serviceWorker.register(`${Discourse.BaseUri}/service-worker.js`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -129,6 +129,58 @@ function wrapAgo(dateStr) {
|
||||
return I18n.t("dates.wrap_ago", { date: dateStr });
|
||||
}
|
||||
|
||||
export function durationTiny(distance, ageOpts) {
|
||||
if (typeof(distance) !== 'number') { return '—'; }
|
||||
|
||||
const dividedDistance = Math.round(distance / 60.0);
|
||||
const distanceInMinutes = (dividedDistance < 1) ? 1 : dividedDistance;
|
||||
|
||||
const t = function(key, opts) {
|
||||
const result = I18n.t("dates.tiny." + key, opts);
|
||||
return (ageOpts && ageOpts.addAgo) ? wrapAgo(result) : result;
|
||||
};
|
||||
|
||||
let formatted;
|
||||
|
||||
switch(true) {
|
||||
case(distance <= 59):
|
||||
formatted = t("less_than_x_minutes", {count: 1});
|
||||
break;
|
||||
case(distanceInMinutes >= 0 && distanceInMinutes <= 44):
|
||||
formatted = t("x_minutes", {count: distanceInMinutes});
|
||||
break;
|
||||
case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
|
||||
formatted = t("about_x_hours", {count: 1});
|
||||
break;
|
||||
case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
|
||||
formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
|
||||
break;
|
||||
case(distanceInMinutes >= 1410 && distanceInMinutes <= 2519):
|
||||
formatted = t("x_days", {count: 1});
|
||||
break;
|
||||
case(distanceInMinutes >= 2520 && distanceInMinutes <= 129599):
|
||||
formatted = t("x_days", {count: Math.round(distanceInMinutes / 1440.0)});
|
||||
break;
|
||||
case(distanceInMinutes >= 129600 && distanceInMinutes <= 525599):
|
||||
formatted = t("x_months", {count: Math.round(distanceInMinutes / 43200.0)});
|
||||
break;
|
||||
default:
|
||||
const numYears = distanceInMinutes / 525600.0;
|
||||
const remainder = numYears % 1;
|
||||
if (remainder < 0.25) {
|
||||
formatted = t("about_x_years", {count: parseInt(numYears)});
|
||||
} else if (remainder < 0.75) {
|
||||
formatted = t("over_x_years", {count: parseInt(numYears)});
|
||||
} else {
|
||||
formatted = t("almost_x_years", {count: parseInt(numYears) + 1});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
function relativeAgeTiny(date, ageOpts) {
|
||||
const format = "tiny";
|
||||
const distance = Math.round((new Date() - date) / 1000);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
import { formatUsername } from 'discourse/lib/utilities';
|
||||
|
||||
function replaceSpan($e, username, opts) {
|
||||
let extra = "";
|
||||
@ -16,7 +17,7 @@ function replaceSpan($e, username, opts) {
|
||||
extra = `data-name='${username}'`;
|
||||
extraClass = "cannot-see";
|
||||
}
|
||||
$e.replaceWith(`<a href='${userPath(username.toLowerCase())}' class='mention ${extraClass}' ${extra}>@${username}</a>`);
|
||||
$e.replaceWith(`<a href='${userPath(username.toLowerCase())}' class='mention ${extraClass}' ${extra}>@${formatUsername(username)}</a>`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,10 +20,11 @@ import { addPostTransformCallback } from 'discourse/widgets/post-stream';
|
||||
import { attachAdditionalPanel } from 'discourse/widgets/header';
|
||||
import { registerIconRenderer, replaceIcon } from 'discourse-common/lib/icon-library';
|
||||
import { addNavItem } from 'discourse/models/nav-item';
|
||||
|
||||
import { replaceFormatter } from 'discourse/lib/utilities';
|
||||
import { modifySelectKit } from "select-kit/mixins/plugin-api";
|
||||
|
||||
// If you add any methods to the API ensure you bump up this number
|
||||
const PLUGIN_API_VERSION = '0.8.11';
|
||||
const PLUGIN_API_VERSION = '0.8.13';
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
@ -570,6 +571,40 @@ class PluginApi {
|
||||
addNavItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Registers a function that will format a username when displayed. This will not
|
||||
* be applied when the username is used as an `id` or in URL strings.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // display usernames in UPPER CASE
|
||||
* api.formatUsername(username => username.toUpperCase());
|
||||
*
|
||||
* ```
|
||||
*
|
||||
**/
|
||||
formatUsername(fn) {
|
||||
replaceFormatter(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Access SelectKit plugin api
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* modifySelectKit("topic-footer-mobile-dropdown").appendContent(() => [{
|
||||
* name: "discourse",
|
||||
* id: 1
|
||||
* }])
|
||||
*/
|
||||
modifySelectKit(pluginApiKey) {
|
||||
return modifySelectKit(pluginApiKey);
|
||||
}
|
||||
}
|
||||
|
||||
let _pluginv01;
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { extractDomainFromUrl } from 'discourse/lib/utilities';
|
||||
import { h } from 'virtual-dom';
|
||||
|
||||
const _decorators = [];
|
||||
@ -7,24 +6,23 @@ export function addFeaturedLinkMetaDecorator(decorator) {
|
||||
_decorators.push(decorator);
|
||||
}
|
||||
|
||||
function extractLinkMeta(topic) {
|
||||
const href = topic.featured_link,
|
||||
target = Discourse.User.currentProp('external_links_in_new_tab') ? '_blank' : '';
|
||||
export function extractLinkMeta(topic) {
|
||||
const href = topic.get('featured_link');
|
||||
const target = Discourse.User.currentProp('external_links_in_new_tab') ? '_blank' : '';
|
||||
|
||||
if (!href) { return; }
|
||||
|
||||
let domain = extractDomainFromUrl(href);
|
||||
if (!domain) { return; }
|
||||
const meta = {
|
||||
target: target,
|
||||
href,
|
||||
domain: topic.get('featured_link_root_domain'),
|
||||
rel: 'nofollow'
|
||||
};
|
||||
|
||||
// www appears frequently, so we truncate it
|
||||
if (domain && domain.substr(0, 4) === 'www.') {
|
||||
domain = domain.substring(4);
|
||||
}
|
||||
|
||||
const meta = { target, href, domain, rel: 'nofollow' };
|
||||
if (_decorators.length) {
|
||||
_decorators.forEach(cb => cb(meta));
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
@ -45,4 +43,3 @@ export function topicFeaturedLinkNode(topic) {
|
||||
}, meta.domain);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user