Version bump

This commit is contained in:
Neil Lalonde 2017-11-30 16:33:01 -05:00
commit 0c40e2dddf
809 changed files with 16233 additions and 9571 deletions

View File

@ -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
View 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'

View File

@ -6,6 +6,7 @@ AllCops:
- 'bundle/**/*'
- 'vendor/**/*'
- 'node_modules/**/*'
- 'public/**/*'
# Prefer &&/|| over and/or.
Style/AndOr:

View File

@ -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'

View File

@ -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

View File

@ -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")
});

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
tagName: ''
});

View File

@ -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() {

View File

@ -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");
});
}
});

View File

@ -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',

View File

@ -0,0 +1,4 @@
export default Ember.Controller.extend({
loading: false,
period: "all"
});

View File

@ -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();
},

View File

@ -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'));

View File

@ -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(' ');
}
});

View File

@ -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');
});
},
}
});

View File

@ -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');
});
}
}
});

View File

@ -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));
}
}
});

View File

@ -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');
});
}
}
});

View File

@ -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,
});

View File

@ -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;

View File

@ -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 });
}
});

View File

@ -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' } );

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
redirect: function() {
this.transitionTo('adminUsersList');
}
});

View File

@ -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'),

View File

@ -27,7 +27,7 @@
</ul>
<div class='boxed white admin-content'>
<div class='admin-contents'>
<div class='admin-contents {{adminContentsClassName}}'>
{{outlet}}
</div>
</div>

View File

@ -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>

View File

@ -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

View File

@ -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}}

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View 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}}

View File

@ -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>

View File

@ -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}}

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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"}}

View File

@ -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>

View File

@ -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}}

View File

@ -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>

View File

@ -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}}

View File

@ -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>

View File

@ -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

View File

@ -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 }
};

View File

@ -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,

View File

@ -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});
}
});
}
});

View File

@ -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);
}
});
},

View File

@ -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);
},

View File

@ -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));
});
}

View File

@ -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") {

View File

@ -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) {

View File

@ -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)
);
}
);
}
});

View File

@ -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);

View File

@ -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 });
}
});

View File

@ -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() {

View File

@ -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});
}
}
}

View File

@ -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);
}
}
});

View File

@ -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';

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
tagName: 'li'
});

View File

@ -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;
});
}
});

View File

@ -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();

View File

@ -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>");
}

View File

@ -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');
});
},

View File

@ -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", () => {

View File

@ -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;

View File

@ -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)

View File

@ -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,

View File

@ -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();
}
}
});

View File

@ -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 };
}

View File

@ -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));
}
}

View File

@ -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();

View File

@ -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);

View File

@ -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')

View File

@ -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"));
}
});

View File

@ -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) {

View File

@ -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'));

View File

@ -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 });
}
});

View File

@ -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 });
}
});

View File

@ -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,

View File

@ -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'));
});
}
}
});

View File

@ -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);
}
}

View File

@ -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'),

View File

@ -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() {

View File

@ -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'),

View File

@ -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
);
}
}

View File

@ -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;
}
});

View File

@ -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 });
}
});

View File

@ -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));
});

View File

@ -0,0 +1,4 @@
import { registerUnbound } from 'discourse-common/lib/helpers';
import { formatUsername } from 'discourse/lib/utilities';
export default registerUnbound('format-username', formatUsername);

View File

@ -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 {

View File

@ -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: './'});
}
}
};

View File

@ -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`);
}
}
};

View File

@ -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 '&mdash;'; }
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);

View File

@ -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>`);
}
}

View File

@ -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;

View File

@ -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