Merge branch 'master' into search_posts_by_filetype
This commit is contained in:
commit
d8c27e3871
|
@ -11,7 +11,6 @@ lib/javascripts/messageformat.js
|
|||
lib/javascripts/moment.js
|
||||
lib/javascripts/moment_locale/
|
||||
lib/highlight_js/
|
||||
lib/es6_module_transpiler/support/es6-module-transpiler.js
|
||||
public/javascripts/
|
||||
spec/phantom_js/smoke_test.js
|
||||
vendor/
|
||||
|
|
15
.eslintrc
15
.eslintrc
|
@ -6,28 +6,27 @@
|
|||
"browser": true,
|
||||
"builtin": true
|
||||
},
|
||||
ecmaVersion: 7,
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 7,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"globals":
|
||||
{"Ember":true,
|
||||
"jQuery":true,
|
||||
"$":true,
|
||||
"QUnit":true,
|
||||
"RSVP":true,
|
||||
"Discourse":true,
|
||||
"Em":true,
|
||||
"Handlebars":true,
|
||||
"I18n":true,
|
||||
"bootbox":true,
|
||||
"module":true,
|
||||
"moduleFor":true,
|
||||
"moduleForComponent":true,
|
||||
"Pretender":true,
|
||||
"sandbox":true,
|
||||
"controllerFor":true,
|
||||
"test":true,
|
||||
"ok":true,
|
||||
"not":true,
|
||||
"expect":true,
|
||||
"equal":true,
|
||||
"visit":true,
|
||||
"andThen":true,
|
||||
"click":true,
|
||||
|
@ -48,12 +47,8 @@
|
|||
"find":true,
|
||||
"sinon":true,
|
||||
"moment":true,
|
||||
"start":true,
|
||||
"_":true,
|
||||
"alert":true,
|
||||
"containsInstance":true,
|
||||
"deepEqual":true,
|
||||
"notEqual":true,
|
||||
"define":true,
|
||||
"require":true,
|
||||
"requirejs":true,
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
skip_missing_workers: true
|
||||
allow_lossy: false
|
||||
# PNG
|
||||
advpng: false
|
||||
optipng:
|
||||
level: 2
|
||||
pngcrush: false
|
||||
pngout: false
|
||||
pngquant: false
|
||||
# JPG
|
||||
jpegrecompress: false
|
||||
timeout: 15
|
|
@ -6,9 +6,7 @@ env:
|
|||
- RUBY_GC_MALLOC_LIMIT=50000000
|
||||
matrix:
|
||||
- "RAILS_MASTER=0 QUNIT_RUN=0"
|
||||
- "RAILS_MASTER=1 QUNIT_RUN=0"
|
||||
- "RAILS_MASTER=0 QUNIT_RUN=1"
|
||||
- "RAILS_MASTER=1 QUNIT_RUN=1"
|
||||
|
||||
addons:
|
||||
postgresql: 9.5
|
||||
|
@ -20,9 +18,6 @@ addons:
|
|||
- jhead
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: "RAILS_MASTER=1 QUNIT_RUN=0"
|
||||
- env: "RAILS_MASTER=1 QUNIT_RUN=1"
|
||||
fast_finish: true
|
||||
|
||||
rvm:
|
||||
|
@ -62,4 +57,4 @@ install:
|
|||
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
|
||||
|
||||
script:
|
||||
- bash -c "if [ '$QUNIT_RUN' == '0' ]; then bundle exec rspec && bundle exec rake plugin:spec; else bundle exec rake qunit:test['200000']; fi"
|
||||
- bash -c "if [ '$QUNIT_RUN' == '0' ]; then bundle exec rspec && bundle exec rake plugin:spec; else LOAD_PLUGINS=1 bundle exec rake qunit:test['300000']; fi"
|
||||
|
|
|
@ -26,12 +26,6 @@ source_file = plugins/poll/config/locales/server.en.yml
|
|||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.imgurserverenyml]
|
||||
file_filter = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.<lang>.yml
|
||||
source_file = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.narrativeclientenyml]
|
||||
file_filter = plugins/discourse-narrative-bot/config/locales/client.<lang>.yml
|
||||
source_file = plugins/discourse-narrative-bot/config/locales/client.en.yml
|
||||
|
|
11
Gemfile
11
Gemfile
|
@ -36,6 +36,7 @@ end
|
|||
|
||||
gem 'mail'
|
||||
gem 'mime-types', require: 'mime/types/columnar'
|
||||
gem 'mini_mime'
|
||||
|
||||
gem 'hiredis'
|
||||
gem 'redis', require: ["redis", "redis/connection/hiredis"]
|
||||
|
@ -51,7 +52,6 @@ gem 'ember-rails', '0.18.5'
|
|||
gem 'ember-source'
|
||||
gem 'ember-handlebars-template', '0.7.5'
|
||||
gem 'barber'
|
||||
gem 'babel-transpiler'
|
||||
|
||||
gem 'message_bus'
|
||||
|
||||
|
@ -74,6 +74,10 @@ gem 'discourse_image_optim', require: 'image_optim'
|
|||
gem 'multi_json'
|
||||
gem 'mustache'
|
||||
gem 'nokogiri'
|
||||
|
||||
# this may end up deprecating nokogiri
|
||||
gem 'oga', require: false
|
||||
|
||||
gem 'omniauth'
|
||||
gem 'omniauth-openid'
|
||||
gem 'openid-redis-store'
|
||||
|
@ -94,13 +98,13 @@ gem 'r2', '~> 0.2.5', require: false
|
|||
gem 'rake'
|
||||
|
||||
gem 'thor', require: false
|
||||
gem 'rest-client'
|
||||
gem 'rinku'
|
||||
gem 'sanitize'
|
||||
gem 'sidekiq'
|
||||
|
||||
# for sidekiq web
|
||||
gem 'sinatra', require: false
|
||||
gem 'tilt', require: false
|
||||
|
||||
gem 'execjs', require: false
|
||||
gem 'mini_racer'
|
||||
gem 'highline', require: false
|
||||
|
@ -118,7 +122,6 @@ group :test do
|
|||
gem 'webmock', require: false
|
||||
gem 'fakeweb', '~> 1.3.0', require: false
|
||||
gem 'minitest', require: false
|
||||
gem 'timecop'
|
||||
# TODO: Remove once we upgrade to Rails 5.
|
||||
gem 'test_after_commit'
|
||||
end
|
||||
|
|
52
Gemfile.lock
52
Gemfile.lock
|
@ -42,17 +42,15 @@ GEM
|
|||
annotate (2.7.2)
|
||||
activerecord (>= 3.2, < 6.0)
|
||||
rake (>= 10.4, < 13.0)
|
||||
ansi (1.5.0)
|
||||
arel (6.0.4)
|
||||
ast (2.3.0)
|
||||
aws-sdk (2.5.3)
|
||||
aws-sdk-resources (= 2.5.3)
|
||||
aws-sdk-core (2.5.3)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-resources (2.5.3)
|
||||
aws-sdk-core (= 2.5.3)
|
||||
babel-source (5.8.34)
|
||||
babel-transpiler (0.7.0)
|
||||
babel-source (>= 4.0, < 6)
|
||||
execjs (~> 2.0)
|
||||
barber (0.11.2)
|
||||
ember-source (>= 1.0, < 3)
|
||||
execjs (>= 1.2, < 3)
|
||||
|
@ -86,8 +84,6 @@ GEM
|
|||
image_size (~> 1.5)
|
||||
in_threads (~> 1.3)
|
||||
progress (~> 3.0, >= 3.0.1)
|
||||
domain_name (0.5.20170404)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
email_reply_trimmer (0.1.6)
|
||||
ember-data-source (2.2.1)
|
||||
ember-source (>= 1.8, < 3.0)
|
||||
|
@ -101,7 +97,7 @@ GEM
|
|||
ember-source (>= 1.1.0)
|
||||
jquery-rails (>= 1.0.17)
|
||||
railties (>= 3.1)
|
||||
ember-source (2.10.2)
|
||||
ember-source (2.13.3)
|
||||
erubis (2.7.0)
|
||||
excon (0.56.0)
|
||||
execjs (2.7.0)
|
||||
|
@ -130,8 +126,6 @@ GEM
|
|||
highline (1.7.8)
|
||||
hiredis (0.6.1)
|
||||
htmlentities (4.3.4)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
http_accept_language (2.0.5)
|
||||
i18n (0.8.4)
|
||||
image_size (1.5.0)
|
||||
|
@ -143,7 +137,7 @@ GEM
|
|||
thor (>= 0.14, < 2.0)
|
||||
jwt (1.5.6)
|
||||
kgio (2.11.0)
|
||||
libv8 (5.3.332.38.5)
|
||||
libv8 (5.7.492.65.1)
|
||||
listen (3.1.5)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
|
@ -152,7 +146,7 @@ GEM
|
|||
loofah (2.0.3)
|
||||
nokogiri (>= 1.5.9)
|
||||
lru_redux (1.1.0)
|
||||
mail (2.6.6.rc1)
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
memory_profiler (0.9.8)
|
||||
message_bus (2.0.2)
|
||||
|
@ -160,9 +154,10 @@ GEM
|
|||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mime-types (2.99.3)
|
||||
mini_mime (0.1.3)
|
||||
mini_portile2 (2.2.0)
|
||||
mini_racer (0.1.9)
|
||||
libv8 (~> 5.3)
|
||||
mini_racer (0.1.11)
|
||||
libv8 (~> 5.7)
|
||||
minitest (5.10.2)
|
||||
mocha (1.2.1)
|
||||
metaclass (~> 0.0.1)
|
||||
|
@ -173,7 +168,6 @@ GEM
|
|||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
mustache (1.0.5)
|
||||
netrc (0.11.0)
|
||||
nokogiri (1.8.0)
|
||||
mini_portile2 (~> 2.2.0)
|
||||
nokogumbo (1.4.13)
|
||||
|
@ -185,6 +179,9 @@ GEM
|
|||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
oga (2.10)
|
||||
ast
|
||||
ruby-ll (~> 2.1)
|
||||
oj (3.1.0)
|
||||
omniauth (1.6.1)
|
||||
hashie (>= 3.4.6, < 3.6.0)
|
||||
|
@ -214,7 +211,7 @@ GEM
|
|||
omniauth-twitter (1.3.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.11)
|
||||
onebox (1.8.16)
|
||||
fast_blank (>= 1.0.0)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
|
@ -236,7 +233,7 @@ GEM
|
|||
pry-rails (0.3.4)
|
||||
pry (>= 0.9.10)
|
||||
public_suffix (2.0.5)
|
||||
puma (3.6.0)
|
||||
puma (3.9.1)
|
||||
r2 (0.2.6)
|
||||
rack (1.6.8)
|
||||
rack-mini-profiler (0.10.5)
|
||||
|
@ -288,10 +285,6 @@ GEM
|
|||
redis (3.3.3)
|
||||
redis-namespace (1.5.3)
|
||||
redis (~> 3.0, >= 3.0.4)
|
||||
rest-client (1.8.0)
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 3.0)
|
||||
netrc (~> 0.7)
|
||||
rinku (2.0.2)
|
||||
rmmseg-cpp (0.2.9)
|
||||
rspec (3.6.0)
|
||||
|
@ -319,6 +312,9 @@ GEM
|
|||
rspec-support (~> 3.6.0)
|
||||
rspec-support (3.6.0)
|
||||
rtlit (0.0.5)
|
||||
ruby-ll (2.1.2)
|
||||
ansi
|
||||
ast
|
||||
ruby-openid (2.7.0)
|
||||
ruby-readability (0.7.0)
|
||||
guess_html_encoding (>= 0.0.4)
|
||||
|
@ -343,16 +339,12 @@ GEM
|
|||
shoulda-context (1.2.2)
|
||||
shoulda-matchers (2.8.0)
|
||||
activesupport (>= 3.0.0)
|
||||
sidekiq (5.0.2)
|
||||
sidekiq (5.0.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (~> 3.3, >= 3.3.3)
|
||||
simple-rss (1.3.1)
|
||||
sinatra (1.4.8)
|
||||
rack (~> 1.5)
|
||||
rack-protection (~> 1.4)
|
||||
tilt (>= 1.3, < 3)
|
||||
slop (3.6.0)
|
||||
spork (1.0.0rc4)
|
||||
spork-rails (4.0.0)
|
||||
|
@ -371,7 +363,6 @@ GEM
|
|||
thor (0.19.4)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.7)
|
||||
timecop (0.8.1)
|
||||
trollop (2.1.2)
|
||||
tzinfo (1.2.3)
|
||||
thread_safe (~> 0.1)
|
||||
|
@ -396,7 +387,6 @@ DEPENDENCIES
|
|||
active_model_serializers (~> 0.8.3)
|
||||
annotate
|
||||
aws-sdk
|
||||
babel-transpiler
|
||||
barber
|
||||
better_errors
|
||||
binding_of_caller
|
||||
|
@ -432,6 +422,7 @@ DEPENDENCIES
|
|||
memory_profiler
|
||||
message_bus
|
||||
mime-types
|
||||
mini_mime
|
||||
mini_racer
|
||||
minitest
|
||||
mocha
|
||||
|
@ -439,6 +430,7 @@ DEPENDENCIES
|
|||
multi_json
|
||||
mustache
|
||||
nokogiri
|
||||
oga
|
||||
oj
|
||||
omniauth
|
||||
omniauth-facebook
|
||||
|
@ -465,7 +457,6 @@ DEPENDENCIES
|
|||
rbtrace
|
||||
redis
|
||||
redis-namespace
|
||||
rest-client
|
||||
rinku
|
||||
rmmseg-cpp
|
||||
rspec
|
||||
|
@ -479,16 +470,15 @@ DEPENDENCIES
|
|||
shoulda
|
||||
sidekiq
|
||||
simple-rss
|
||||
sinatra
|
||||
spork-rails
|
||||
stackprof
|
||||
test_after_commit
|
||||
thor
|
||||
timecop
|
||||
tilt
|
||||
uglifier
|
||||
unf
|
||||
unicorn
|
||||
webmock
|
||||
|
||||
BUNDLED WITH
|
||||
1.14.6
|
||||
1.15.1
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
import AdminUser from 'admin/models/admin-user';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["ip-lookup"],
|
||||
|
@ -44,7 +45,6 @@ export default Ember.Component.extend({
|
|||
self.set("totalOthersWithSameIP", result.total);
|
||||
});
|
||||
|
||||
const AdminUser = require('admin/models/admin-user').default;
|
||||
AdminUser.findAll("active", data).then(function (users) {
|
||||
self.setProperties({
|
||||
other_accounts: users,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import Permalink from 'admin/models/permalink';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['permalink-form'],
|
||||
formSubmitted: false,
|
||||
|
@ -18,8 +20,6 @@ export default Ember.Component.extend({
|
|||
|
||||
actions: {
|
||||
submit: function() {
|
||||
const Permalink = require('admin/models/permalink').default;
|
||||
|
||||
if (!this.get('formSubmitted')) {
|
||||
const self = this;
|
||||
self.set('formSubmitted', true);
|
||||
|
|
|
@ -2,11 +2,13 @@ import EmailPreview from 'admin/models/email-preview';
|
|||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
username: null,
|
||||
lastSeen: null,
|
||||
|
||||
emailEmpty: Em.computed.empty('email'),
|
||||
sendEmailDisabled: Em.computed.or('emailEmpty', 'sendingEmail'),
|
||||
showSendEmailForm: Em.computed.notEmpty('model.html_content'),
|
||||
htmlEmpty: Em.computed.empty('model.html_content'),
|
||||
emailEmpty: Ember.computed.empty('email'),
|
||||
sendEmailDisabled: Ember.computed.or('emailEmpty', 'sendingEmail'),
|
||||
showSendEmailForm: Ember.computed.notEmpty('model.html_content'),
|
||||
htmlEmpty: Ember.computed.empty('model.html_content'),
|
||||
|
||||
actions: {
|
||||
refresh() {
|
||||
|
@ -14,7 +16,14 @@ export default Ember.Controller.extend({
|
|||
|
||||
this.set('loading', true);
|
||||
this.set('sentEmail', false);
|
||||
EmailPreview.findDigest(this.get('lastSeen'), this.get('username')).then(email => {
|
||||
|
||||
let username = this.get('username');
|
||||
if (!username) {
|
||||
username = this.currentUser.get('username');
|
||||
this.set('username', username);
|
||||
}
|
||||
|
||||
EmailPreview.findDigest(username, this.get('lastSeen')).then(email => {
|
||||
model.setProperties(email.getProperties('html_content', 'text_content'));
|
||||
this.set('loading', false);
|
||||
});
|
||||
|
@ -28,16 +37,14 @@ export default Ember.Controller.extend({
|
|||
this.set('sendingEmail', true);
|
||||
this.set('sentEmail', false);
|
||||
|
||||
const self = this;
|
||||
|
||||
EmailPreview.sendDigest(this.get('lastSeen'), this.get('username'), this.get('email')).then(result => {
|
||||
EmailPreview.sendDigest(this.get('username'), this.get('lastSeen'), this.get('email')).then(result => {
|
||||
if (result.errors) {
|
||||
bootbox.alert(result.errors);
|
||||
} else {
|
||||
self.set('sentEmail', true);
|
||||
this.set('sentEmail', true);
|
||||
}
|
||||
}).catch(popupAjaxError).finally(function() {
|
||||
self.set('sendingEmail', false);
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('sendingEmail', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,15 @@ export default Ember.Controller.extend({
|
|||
];
|
||||
}.property(),
|
||||
|
||||
visibilityLevelOptions: function() {
|
||||
return [
|
||||
{ name: I18n.t("groups.visibility_levels.public"), value: 0 },
|
||||
{ name: I18n.t("groups.visibility_levels.members"), value: 1 },
|
||||
{ name: I18n.t("groups.visibility_levels.staff"), value: 2 },
|
||||
{ name: I18n.t("groups.visibility_levels.owners"), value: 3 }
|
||||
];
|
||||
}.property(),
|
||||
|
||||
trustLevelOptions: function() {
|
||||
return [
|
||||
{ name: I18n.t("groups.trust_levels.none"), value: 0 },
|
||||
|
@ -22,14 +31,16 @@ export default Ember.Controller.extend({
|
|||
];
|
||||
}.property(),
|
||||
|
||||
@computed('model.visible', 'model.public')
|
||||
disableMembershipRequestSetting(visible, publicGroup) {
|
||||
return !visible || publicGroup;
|
||||
@computed('model.visibility_level', 'model.public')
|
||||
disableMembershipRequestSetting(visibility_level, publicGroup) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return (visibility_level !== 0) || publicGroup;
|
||||
},
|
||||
|
||||
@computed('model.visible', 'model.allow_membership_requests')
|
||||
disablePublicSetting(visible, allowMembershipRequests) {
|
||||
return !visible || allowMembershipRequests;
|
||||
@computed('model.visibility_level', 'model.allow_membership_requests')
|
||||
disablePublicSetting(visibility_level, allowMembershipRequests) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return (visibility_level !== 0) || allowMembershipRequests;
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import AdminUser from 'admin/models/admin-user';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
const ApiKey = Discourse.Model.extend({
|
||||
|
||||
/**
|
||||
|
@ -36,8 +38,7 @@ ApiKey.reopenClass({
|
|||
@param {...} var_args the properties to initialize this with
|
||||
@returns {ApiKey} the ApiKey instance
|
||||
**/
|
||||
create: function() {
|
||||
const AdminUser = require('admin/models/admin-user').default;
|
||||
create() {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (result.user) {
|
||||
result.user = AdminUser.create(result.user);
|
||||
|
|
|
@ -1,42 +1,24 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
const EmailPreview = Discourse.Model.extend({});
|
||||
|
||||
export function oneWeekAgo() {
|
||||
return moment().locale('en').subtract(7, 'days').format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
EmailPreview.reopenClass({
|
||||
findDigest: function(lastSeenAt, username) {
|
||||
|
||||
if (Em.isEmpty(lastSeenAt)) {
|
||||
lastSeenAt = this.oneWeekAgo();
|
||||
}
|
||||
|
||||
if (Em.isEmpty(username)) {
|
||||
username = Discourse.User.current().username;
|
||||
}
|
||||
|
||||
findDigest(username, lastSeenAt) {
|
||||
return ajax("/admin/email/preview-digest.json", {
|
||||
data: { last_seen_at: lastSeenAt, username: username }
|
||||
}).then(function (result) {
|
||||
return EmailPreview.create(result);
|
||||
});
|
||||
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username }
|
||||
}).then(result => EmailPreview.create(result));
|
||||
},
|
||||
|
||||
sendDigest: function(lastSeenAt, username, email) {
|
||||
if (Em.isEmpty(lastSeenAt)) {
|
||||
lastSeenAt = this.oneWeekAgo();
|
||||
}
|
||||
|
||||
if (Em.isEmpty(username)) {
|
||||
username = Discourse.User.current().username;
|
||||
}
|
||||
|
||||
sendDigest(username, lastSeenAt, email) {
|
||||
return ajax("/admin/email/send-digest.json", {
|
||||
data: { last_seen_at: lastSeenAt, username: username, email: email }
|
||||
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username, email }
|
||||
});
|
||||
},
|
||||
|
||||
oneWeekAgo() {
|
||||
const en = moment().locale('en');
|
||||
return en.subtract(7, 'days').format('YYYY-MM-DD');
|
||||
}
|
||||
});
|
||||
|
||||
export default EmailPreview;
|
||||
|
|
|
@ -37,7 +37,7 @@ export default RestModel.extend({
|
|||
},
|
||||
|
||||
groupFinder(term) {
|
||||
return Group.findAll({search: term, ignore_automatic: false});
|
||||
return Group.findAll({ term: term, ignore_automatic: false });
|
||||
},
|
||||
|
||||
@computed('wildcard_web_hook', 'web_hook_event_types.[]')
|
||||
|
@ -82,4 +82,3 @@ export default RestModel.extend({
|
|||
return this.createProperties();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import EmailPreview from 'admin/models/email-preview';
|
||||
import { default as EmailPreview, oneWeekAgo } from 'admin/models/email-preview';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
|
||||
model() {
|
||||
return EmailPreview.findDigest();
|
||||
return EmailPreview.findDigest(this.currentUser.get('username'));
|
||||
},
|
||||
|
||||
afterModel(model) {
|
||||
const controller = this.controllerFor('adminEmailPreviewDigest');
|
||||
controller.setProperties({
|
||||
model: model,
|
||||
lastSeen: moment().subtract(7, 'days').format('YYYY-MM-DD'),
|
||||
model,
|
||||
username: this.currentUser.get('username'),
|
||||
lastSeen: oneWeekAgo(),
|
||||
showHtml: true
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ export default Discourse.Route.extend({
|
|||
|
||||
model(params) {
|
||||
if (params.name === 'new') {
|
||||
return Group.create({ automatic: false, visible: true });
|
||||
return Group.create({ automatic: false, visibility_level: 0 });
|
||||
}
|
||||
|
||||
const group = this.modelFor('adminGroupsType').findBy('name', params.name);
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
/**
|
||||
Handles routes for admin reports
|
||||
import Report from 'admin/models/report';
|
||||
|
||||
@class AdminReportsRoute
|
||||
@extends Discourse.Route
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
export default Discourse.Route.extend({
|
||||
queryParams: { mode: {}, "start_date": {}, "end_date": {}, "category_id": {}, "group_id": {} },
|
||||
|
||||
model: function(params) {
|
||||
const Report = require('admin/models/report').default;
|
||||
model(params) {
|
||||
return Report.find(params.type, params['start_date'], params['end_date'], params['category_id'], params['group_id']);
|
||||
},
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
{{text-field value=value classNames="input-setting-string"}}
|
||||
{{#if setting.textarea}}
|
||||
{{textarea value=value classNames="input-setting-textarea"}}
|
||||
{{else}}
|
||||
{{text-field value=value classNames="input-setting-string"}}
|
||||
{{/if}}
|
||||
|
||||
{{setting-validation-message message=validationMessage}}
|
||||
<div class='desc'>{{{unbound setting.description}}}</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<p>{{i18n 'admin.email.preview_digest_desc'}}</p>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<div class='admin-controls email-preview'>
|
||||
<div class='span7 controls'>
|
||||
<label for='last-seen'>{{i18n 'admin.email.last_seen_user'}}</label>
|
||||
{{date-picker-past value=lastSeen id="last-seen"}}
|
||||
<label>{{i18n 'admin.email.user'}}:</label>
|
||||
{{user-selector single="true" usernames=username}}
|
||||
{{user-selector single="true" usernames=username canReceiveUpdates="true"}}
|
||||
<button class='btn' {{action "refresh"}}>{{i18n 'admin.email.refresh'}}</button>
|
||||
<div class="toggle">
|
||||
<label>{{i18n 'admin.email.format'}}</label>
|
||||
|
|
|
@ -43,10 +43,8 @@
|
|||
{{/if}}
|
||||
|
||||
<div>
|
||||
<label>
|
||||
{{input type="checkbox" checked=model.visible}}
|
||||
{{i18n 'groups.visible'}}
|
||||
</label>
|
||||
<label for="visiblity">{{i18n 'groups.visibility_levels.title'}}</label>
|
||||
{{combo-box name="alias" valueAttribute="value" value=model.visibility_level content=visibilityLevelOptions}}
|
||||
</div>
|
||||
|
||||
{{#unless model.automatic}}
|
||||
|
|
|
@ -27,15 +27,21 @@
|
|||
//= require ./discourse/lib/eyeline
|
||||
//= require ./discourse/lib/show-modal
|
||||
//= require ./discourse/mixins/scrolling
|
||||
//= require ./discourse/lib/ajax-error
|
||||
//= require ./discourse/models/model
|
||||
//= require ./discourse/models/rest
|
||||
//= require ./discourse/models/result-set
|
||||
//= require ./discourse/models/store
|
||||
//= require ./discourse/models/action-summary
|
||||
//= require ./discourse/models/topic
|
||||
//= require ./discourse/models/draft
|
||||
//= require ./discourse/models/composer
|
||||
//= require ./discourse/models/badge-grouping
|
||||
//= require ./discourse/models/badge
|
||||
//= require ./discourse/models/permission-type
|
||||
//= require ./discourse/models/user-action-group
|
||||
//= require ./discourse/models/category
|
||||
//= require ./discourse/models/input-validation
|
||||
//= require ./discourse/lib/ajax-error
|
||||
//= require ./discourse/lib/search
|
||||
//= require ./discourse/lib/user-search
|
||||
//= require ./discourse/lib/export-csv
|
||||
|
@ -44,10 +50,7 @@
|
|||
//= require ./discourse/lib/debounce
|
||||
//= require ./discourse/lib/safari-hacks
|
||||
//= require_tree ./discourse/adapters
|
||||
//= require ./discourse/models/result-set
|
||||
//= require ./discourse/models/store
|
||||
//= require ./discourse/models/post-action-type
|
||||
//= require ./discourse/models/action-summary
|
||||
//= require ./discourse/models/post
|
||||
//= require ./discourse/lib/posts-with-placeholders
|
||||
//= require ./discourse/models/post-stream
|
||||
|
@ -66,8 +69,6 @@
|
|||
//= require ./discourse/components/notifications-button
|
||||
//= require ./discourse/lib/link-mentions
|
||||
//= require ./discourse/components/site-header
|
||||
//= require ./discourse/lib/emoji/groups
|
||||
//= require ./discourse/lib/emoji/toolbar
|
||||
//= require ./discourse/components/d-editor
|
||||
//= require ./discourse/lib/screen-track
|
||||
//= require ./discourse/routes/discourse
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
// ensure Discourse is added as a global
|
||||
(function() {
|
||||
var Discourse = require('discourse').default;
|
||||
|
||||
Discourse.dialect_deprecated = true;
|
||||
|
||||
window.Discourse = Discourse;
|
||||
window.Discourse = requirejs('discourse').default;
|
||||
})();
|
||||
|
|
|
@ -15,7 +15,11 @@ export function getRegister(obj) {
|
|||
const register = {
|
||||
lookup: (...args) => owner.lookup(...args),
|
||||
lookupFactory: (...args) => {
|
||||
return owner.lookupFactory ? owner.lookupFactory(...args) : owner._lookupFactory(...args);
|
||||
if (owner.factoryFor) {
|
||||
return owner.factoryFor(...args);
|
||||
} else if (owner._lookupFactory) {
|
||||
return owner._lookupFactory(...args);
|
||||
}
|
||||
},
|
||||
|
||||
deprecateContainer(target) {
|
||||
|
|
|
@ -38,7 +38,7 @@ export function buildResolver(baseName) {
|
|||
resolveRouter(parsedName) {
|
||||
const routerPath = `${baseName}/router`;
|
||||
if (requirejs.entries[routerPath]) {
|
||||
const module = require(routerPath, null, null, true);
|
||||
const module = requirejs(routerPath, null, null, true);
|
||||
return module.default;
|
||||
}
|
||||
return this._super(parsedName);
|
||||
|
@ -79,7 +79,7 @@ export function buildResolver(baseName) {
|
|||
|
||||
var module;
|
||||
if (moduleName) {
|
||||
module = require(moduleName, null, null, true /* force sync */);
|
||||
module = requirejs(moduleName, null, null, true /* force sync */);
|
||||
if (module && module['default']) { module = module['default']; }
|
||||
}
|
||||
return module;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var define, require, requirejs;
|
||||
var define, requirejs;
|
||||
|
||||
(function() {
|
||||
|
||||
|
@ -54,7 +54,7 @@ var define, require, requirejs;
|
|||
var name = this.name;
|
||||
|
||||
return this._require || (this._require = function(dep) {
|
||||
return require(resolve(dep, name));
|
||||
return requirejs(resolve(dep, name));
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -127,7 +127,7 @@ var define, require, requirejs;
|
|||
if (!mod) {
|
||||
throw new Error('Could not find module `' + name + '` imported from `' + origin + '`');
|
||||
}
|
||||
return require(name);
|
||||
return requirejs(name);
|
||||
}
|
||||
|
||||
function missingModule(name) {
|
||||
|
|
|
@ -102,7 +102,7 @@ const Discourse = Ember.Application.extend({
|
|||
|
||||
Object.keys(requirejs._eak_seen).forEach(function(key) {
|
||||
if (/\/pre\-initializers\//.test(key)) {
|
||||
const module = require(key, null, null, true);
|
||||
const module = requirejs(key, null, null, true);
|
||||
if (!module) { throw new Error(key + ' must export an initializer.'); }
|
||||
|
||||
const init = module.default;
|
||||
|
@ -117,7 +117,7 @@ const Discourse = Ember.Application.extend({
|
|||
|
||||
Object.keys(requirejs._eak_seen).forEach(function(key) {
|
||||
if (/\/initializers\//.test(key)) {
|
||||
const module = require(key, null, null, true);
|
||||
const module = requirejs(key, null, null, true);
|
||||
if (!module) { throw new Error(key + ' must export an initializer.'); }
|
||||
|
||||
const init = module.default;
|
||||
|
@ -131,7 +131,7 @@ const Discourse = Ember.Application.extend({
|
|||
});
|
||||
|
||||
// Plugins that are registered via `<script>` tags.
|
||||
const withPluginApi = require('discourse/lib/plugin-api').withPluginApi;
|
||||
const withPluginApi = requirejs('discourse/lib/plugin-api').withPluginApi;
|
||||
let initCount = 0;
|
||||
_pluginCallbacks.forEach(function(cb) {
|
||||
Discourse.instanceInitializer({
|
||||
|
|
|
@ -5,6 +5,8 @@ import {
|
|||
SET_BASED_ON_LAST_POST
|
||||
} from "discourse/components/auto-update-input-selector";
|
||||
|
||||
import { PUBLISH_TO_CATEGORY_STATUS_TYPE } from 'discourse/controllers/edit-topic-timer';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
selection: null,
|
||||
date: null,
|
||||
|
@ -62,10 +64,14 @@ export default Ember.Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@computed("statusType", "input", "isCustom", "date", "time", "willCloseImmediately")
|
||||
showTopicStatusInfo(statusType, input, isCustom, date, time, willCloseImmediately) {
|
||||
@computed("statusType", "input", "isCustom", "date", "time", "willCloseImmediately", "categoryId")
|
||||
showTopicStatusInfo(statusType, input, isCustom, date, time, willCloseImmediately, categoryId) {
|
||||
if (!statusType || willCloseImmediately) return false;
|
||||
|
||||
if (statusType === PUBLISH_TO_CATEGORY_STATUS_TYPE && Ember.isEmpty(categoryId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isCustom) {
|
||||
return date || time;
|
||||
} else {
|
||||
|
|
|
@ -39,12 +39,21 @@ export default Ember.Component.extend({
|
|||
if (!this.site.mobileView) { return; }
|
||||
|
||||
let target = $(e.target);
|
||||
|
||||
if (target.hasClass('posts-map')) {
|
||||
if (target.closest('.posts-map').length) {
|
||||
const topicId = target.closest('tr').attr('data-topic-id');
|
||||
if (topicId) {
|
||||
if (target.prop('tagName') !== 'A') {
|
||||
target = target.find('a');
|
||||
let targetLinks = target.find('a');
|
||||
if (targetLinks.length) {
|
||||
target = targetLinks;
|
||||
} else {
|
||||
targetLinks = target.closest('a');
|
||||
if (targetLinks.length) {
|
||||
target = targetLinks;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const topic = this.get('topics').findBy('id', parseInt(topicId));
|
||||
|
|
|
@ -42,7 +42,7 @@ export default Combobox.extend({
|
|||
|
||||
@computed("rootNone", "rootNoneLabel")
|
||||
none(rootNone, rootNoneLabel) {
|
||||
if (Discourse.SiteSettings.allow_uncategorized_topics || this.get('allowUncategorized')) {
|
||||
if (this.siteSettings.allow_uncategorized_topics || this.get('allowUncategorized')) {
|
||||
if (rootNone) {
|
||||
return rootNoneLabel || "category.none";
|
||||
} else {
|
||||
|
|
|
@ -3,8 +3,9 @@ import Composer from 'discourse/models/composer';
|
|||
import afterTransition from 'discourse/lib/after-transition';
|
||||
import positioningWorkaround from 'discourse/lib/safari-hacks';
|
||||
import { headerHeight } from 'discourse/components/site-header';
|
||||
import KeyEnterEscape from 'discourse/mixins/key-enter-escape';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
export default Ember.Component.extend(KeyEnterEscape, {
|
||||
elementId: 'reply-control',
|
||||
|
||||
classNameBindings: ['composer.creatingPrivateMessage:private-message',
|
||||
|
@ -65,17 +66,6 @@ export default Ember.Component.extend({
|
|||
}, 1000);
|
||||
},
|
||||
|
||||
keyDown(e) {
|
||||
if (e.which === 27) {
|
||||
this.sendAction('cancelled');
|
||||
return false;
|
||||
} else if (e.which === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
// CTRL+ENTER or CMD+ENTER
|
||||
this.sendAction('save');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
@observes('composeState')
|
||||
disableFullscreen() {
|
||||
if (this.get('composeState') !== Composer.OPEN && positioningWorkaround.blur) {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import userSearch from 'discourse/lib/user-search';
|
||||
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
|
||||
import { default as computed, on, observes } 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';
|
||||
import Composer from 'discourse/models/composer';
|
||||
import { load } from 'pretty-text/oneboxer';
|
||||
import { applyInlineOneboxes } from 'pretty-text/inline-oneboxer';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import InputValidation from 'discourse/models/input-validation';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
@ -30,6 +31,14 @@ export default Ember.Component.extend({
|
|||
_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')
|
||||
|
@ -42,9 +51,16 @@ export default Ember.Component.extend({
|
|||
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,
|
||||
|
||||
lookupAvatarByPostNumber: (postNumber, topicId) => {
|
||||
const topic = this.get('topic');
|
||||
if (!topic) { return; }
|
||||
|
@ -158,6 +174,10 @@ export default Ember.Component.extend({
|
|||
});
|
||||
},
|
||||
|
||||
_loadInlineOneboxes(inline) {
|
||||
applyInlineOneboxes(inline, ajax);
|
||||
},
|
||||
|
||||
_loadOneboxes($oneboxes) {
|
||||
const post = this.get('composer.post');
|
||||
let refresh = false;
|
||||
|
@ -228,6 +248,8 @@ export default Ember.Component.extend({
|
|||
_bindUploadTarget() {
|
||||
this._unbindUploadTarget(); // in case it's still bound, let's clean it up first
|
||||
|
||||
this._pasted = false;
|
||||
|
||||
const $element = this.$();
|
||||
const csrf = this.session.get('csrfToken');
|
||||
const uploadPlaceholder = this.get('uploadPlaceholder');
|
||||
|
@ -238,10 +260,24 @@ export default Ember.Component.extend({
|
|||
pasteZone: $element,
|
||||
});
|
||||
|
||||
$element.on('fileuploadpaste', () => this._pasted = true);
|
||||
|
||||
$element.on('fileuploadsubmit', (e, data) => {
|
||||
const isUploading = validateUploadedFiles(data.files);
|
||||
const isPrivateMessage = this.get("composer.privateMessage");
|
||||
|
||||
data.formData = { type: "composer" };
|
||||
if (isPrivateMessage) data.formData.for_private_message = true;
|
||||
if (this._pasted) data.formData.pasted = true;
|
||||
|
||||
const opts = {
|
||||
isPrivateMessage,
|
||||
allowStaffToUploadAnyFileInPm: this.siteSettings.allow_staff_to_upload_any_file_in_pm,
|
||||
};
|
||||
|
||||
const isUploading = validateUploadedFiles(data.files, opts);
|
||||
|
||||
this.setProperties({ uploadProgress: 0, isUploading });
|
||||
|
||||
return isUploading;
|
||||
});
|
||||
|
||||
|
@ -250,6 +286,7 @@ export default Ember.Component.extend({
|
|||
});
|
||||
|
||||
$element.on("fileuploadsend", (e, data) => {
|
||||
this._pasted = false;
|
||||
this._validUploads++;
|
||||
this.appEvents.trigger('composer:insert-text', uploadPlaceholder);
|
||||
|
||||
|
@ -428,6 +465,8 @@ 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
|
||||
|
@ -469,7 +508,6 @@ export default Ember.Component.extend({
|
|||
|
||||
togglePreview() {
|
||||
this.toggleProperty('showPreview');
|
||||
this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
|
||||
},
|
||||
|
||||
extraButtons(toolbar) {
|
||||
|
@ -541,6 +579,18 @@ export default Ember.Component.extend({
|
|||
Ember.run.debounce(this, this._loadOneboxes, $oneboxes, 450);
|
||||
}
|
||||
|
||||
let inline = {};
|
||||
$('a.inline-onebox-loading', $preview).each(function(index, link) {
|
||||
let $link = $(link);
|
||||
$link.removeClass('inline-onebox-loading');
|
||||
let text = $link.text();
|
||||
inline[text] = inline[text] || [];
|
||||
inline[text].push($link);
|
||||
});
|
||||
if (Object.keys(inline).length > 0) {
|
||||
Ember.run.debounce(this, this._loadInlineOneboxes, inline, 450);
|
||||
}
|
||||
|
||||
this.trigger('previewRefreshed', $preview);
|
||||
this.sendAction('afterRefresh', $preview);
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decor
|
|||
import InputValidation from 'discourse/models/input-validation';
|
||||
import { load, lookupCache } from 'pretty-text/oneboxer';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import afterTransition from 'discourse/lib/after-transition';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['title-input'],
|
||||
|
@ -10,7 +11,11 @@ export default Ember.Component.extend({
|
|||
didInsertElement() {
|
||||
this._super();
|
||||
if (this.get('focusTarget') === 'title') {
|
||||
this.$('input').putCursorAtEnd();
|
||||
const $input = this.$("input");
|
||||
|
||||
afterTransition(this.$().closest("#reply-control"), () => {
|
||||
$input.putCursorAtEnd();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
|
||||
@computed('composeState')
|
||||
toggleIcon(composeState) {
|
||||
if (composeState === "draft" || composeState === "saving") {
|
||||
return "times";
|
||||
}
|
||||
return "chevron-down";
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { cookAsync } from 'discourse/lib/text';
|
||||
|
||||
const CookText = Ember.Component.extend({
|
||||
tagName: '',
|
||||
cooked: null,
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
cookAsync(this.get('rawText')).then(cooked => this.set('cooked', cooked));
|
||||
}
|
||||
});
|
||||
|
||||
CookText.reopenClass({ positionalParams: ['rawText'] });
|
||||
|
||||
export default CookText;
|
|
@ -1,14 +1,13 @@
|
|||
/*global Mousetrap:true */
|
||||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { showSelector } from "discourse/lib/emoji/toolbar";
|
||||
import Category from 'discourse/models/category';
|
||||
import { categoryHashtagTriggerRule } from 'discourse/lib/category-hashtags';
|
||||
import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags';
|
||||
import { search as searchCategoryTag } from 'discourse/lib/category-tag-search';
|
||||
import { SEPARATOR } from 'discourse/lib/category-hashtags';
|
||||
import { cook } from 'discourse/lib/text';
|
||||
import { cookAsync } from 'discourse/lib/text';
|
||||
import { translations } from 'pretty-text/emoji/data';
|
||||
import { emojiSearch } from 'pretty-text/emoji';
|
||||
import { emojiSearch, isSkinTonableEmoji } from 'pretty-text/emoji';
|
||||
import { emojiUrlFor } from 'discourse/lib/text';
|
||||
import { getRegister } from 'discourse-common/lib/get-owner';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
@ -78,7 +77,11 @@ class Toolbar {
|
|||
group: 'insertions',
|
||||
icon: 'quote-right',
|
||||
shortcut: 'Shift+9',
|
||||
perform: e => e.applySurround('> ', '', 'code_text')
|
||||
perform: e => e.applyList(
|
||||
'> ',
|
||||
'blockquote_text',
|
||||
{ applyEmptyLines: true, multiline: true }
|
||||
)
|
||||
});
|
||||
|
||||
this.addButton({id: 'code', group: 'insertions', shortcut: 'Shift+C', action: 'formatCode'});
|
||||
|
@ -138,7 +141,7 @@ class Toolbar {
|
|||
label: button.label,
|
||||
icon: button.label ? null : button.icon || button.id,
|
||||
action: button.action || 'toolbarButton',
|
||||
perform: button.perform || Ember.K,
|
||||
perform: button.perform || function() { },
|
||||
trimLeading: button.trimLeading
|
||||
};
|
||||
|
||||
|
@ -198,6 +201,7 @@ export default Ember.Component.extend({
|
|||
linkText: '',
|
||||
lastSel: null,
|
||||
_mouseTrap: null,
|
||||
emojiPickerIsActive: false,
|
||||
|
||||
@computed('placeholder')
|
||||
placeholderTranslated(placeholder) {
|
||||
|
@ -247,6 +251,7 @@ export default Ember.Component.extend({
|
|||
});
|
||||
|
||||
if (this.get('composerEvents')) {
|
||||
this.appEvents.on('composer:insert-block', text => this._addBlock(this._getSelected(), text));
|
||||
this.appEvents.on('composer:insert-text', text => this._addText(this._getSelected(), text));
|
||||
this.appEvents.on('composer:replace-text', (oldVal, newVal) => this._replaceText(oldVal, newVal));
|
||||
}
|
||||
|
@ -279,14 +284,14 @@ export default Ember.Component.extend({
|
|||
const value = this.get('value');
|
||||
const markdownOptions = this.get('markdownOptions') || {};
|
||||
|
||||
markdownOptions.siteSettings = this.siteSettings;
|
||||
|
||||
this.set('preview', cook(value));
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
if (this._state !== "inDOM") { return; }
|
||||
const $preview = this.$('.d-editor-preview');
|
||||
if ($preview.length === 0) return;
|
||||
this.sendAction('previewUpdated', $preview);
|
||||
cookAsync(value, markdownOptions).then(cooked => {
|
||||
this.set('preview', cooked);
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
if (this._state !== "inDOM") { return; }
|
||||
const $preview = this.$('.d-editor-preview');
|
||||
if ($preview.length === 0) return;
|
||||
this.sendAction('previewUpdated', $preview);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -327,7 +332,6 @@ export default Ember.Component.extend({
|
|||
_applyEmojiAutocomplete($editorInput) {
|
||||
if (!this.siteSettings.enable_emoji) { return; }
|
||||
|
||||
const register = this.register;
|
||||
const self = this;
|
||||
|
||||
$editorInput.autocomplete({
|
||||
|
@ -337,24 +341,16 @@ export default Ember.Component.extend({
|
|||
self.set('value', text);
|
||||
},
|
||||
|
||||
onKeyUp(text, cp) {
|
||||
return text.substring(0, cp).match(/(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/g);
|
||||
},
|
||||
|
||||
transformComplete(v) {
|
||||
if (v.code) {
|
||||
return `${v.code}:`;
|
||||
} else {
|
||||
showSelector({
|
||||
appendTo: self.$(),
|
||||
register,
|
||||
onSelect: title => {
|
||||
// Remove the previously type characters when a new emoji is selected from the selector.
|
||||
let selected = self._getSelected();
|
||||
let newPre = selected.pre.replace(/:[^:]+$/, ":");
|
||||
let numOfRemovedChars = selected.pre.length - newPre.length;
|
||||
selected.pre = newPre;
|
||||
selected.start -= numOfRemovedChars;
|
||||
selected.end -= numOfRemovedChars;
|
||||
self._addText(selected, `${title}:`);
|
||||
}
|
||||
});
|
||||
$editorInput.autocomplete({cancel: true});
|
||||
self.set('emojiPickerIsActive', true);
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
@ -372,6 +368,20 @@ export default Ember.Component.extend({
|
|||
return resolve([translations[full]]);
|
||||
}
|
||||
|
||||
const match = term.match(/^:?(.*?):t(\d)?$/);
|
||||
if (match) {
|
||||
let name = match[1];
|
||||
let scale = match[2];
|
||||
|
||||
if (isSkinTonableEmoji(name)) {
|
||||
if (scale) {
|
||||
return resolve([`${name}:t${scale}`]);
|
||||
} else {
|
||||
return resolve([2, 3, 4, 5, 6].map(x => `${name}:t${x}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options = emojiSearch(term, {maxResults: 5});
|
||||
|
||||
return resolve(options);
|
||||
|
@ -434,11 +444,15 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
// perform the same operation over many lines of text
|
||||
_getMultilineContents(lines, head, hval, hlen, tail, tlen) {
|
||||
_getMultilineContents(lines, head, hval, hlen, tail, tlen, opts) {
|
||||
let operation = OP.NONE;
|
||||
|
||||
const applyEmptyLines = opts && opts.applyEmptyLines;
|
||||
|
||||
return lines.map(l => {
|
||||
if (l.length === 0) { return l; }
|
||||
if (!applyEmptyLines && l.length === 0) {
|
||||
return l;
|
||||
}
|
||||
|
||||
if (operation !== OP.ADDED &&
|
||||
(l.slice(0, hlen) === hval && tlen === 0 || l.slice(-tlen) === tail)) {
|
||||
|
@ -494,8 +508,15 @@ export default Ember.Component.extend({
|
|||
this.set('value', `${pre.slice(0, -hlen)}${sel.value}${post.slice(tlen)}`);
|
||||
this._selectText(sel.start - hlen, sel.value.length);
|
||||
} else {
|
||||
const contents = this._getMultilineContents(lines, head, hval, hlen, tail, tlen);
|
||||
|
||||
const contents = this._getMultilineContents(
|
||||
lines,
|
||||
head,
|
||||
hval,
|
||||
hlen,
|
||||
tail,
|
||||
tlen,
|
||||
opts
|
||||
);
|
||||
this.set('value', `${pre}${contents}${post}`);
|
||||
if (lines.length === 1 && tlen > 0) {
|
||||
this._selectText(sel.start + hlen, sel.value.length);
|
||||
|
@ -506,9 +527,9 @@ export default Ember.Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
_applyList(sel, head, exampleKey) {
|
||||
_applyList(sel, head, exampleKey, opts) {
|
||||
if (sel.value.indexOf("\n") !== -1) {
|
||||
this._applySurround(sel, head, '', exampleKey);
|
||||
this._applySurround(sel, head, '', exampleKey, opts);
|
||||
} else {
|
||||
|
||||
const [hval, hlen] = getHead(head);
|
||||
|
@ -553,6 +574,36 @@ export default Ember.Component.extend({
|
|||
this._selectText(newSelection.start, newSelection.end - newSelection.start);
|
||||
},
|
||||
|
||||
_addBlock(sel, text) {
|
||||
text = (text || '').trim();
|
||||
if (text.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pre = sel.pre;
|
||||
let post = sel.value + sel.post;
|
||||
|
||||
if (pre.length > 0) {
|
||||
pre = pre.replace(/\n*$/, "\n\n");
|
||||
}
|
||||
|
||||
if (post.length > 0) {
|
||||
post = post.replace(/^\n*/, "\n\n");
|
||||
}
|
||||
|
||||
|
||||
const value = pre + text + post;
|
||||
const $textarea = this.$('textarea.d-editor-input');
|
||||
|
||||
this.set('value', value);
|
||||
|
||||
$textarea.val(value);
|
||||
$textarea.prop("selectionStart", (pre+text).length + 2);
|
||||
$textarea.prop("selectionEnd", (pre+text).length + 2);
|
||||
|
||||
Ember.run.scheduleOnce("afterRender", () => $textarea.focus());
|
||||
},
|
||||
|
||||
_addText(sel, text) {
|
||||
const $textarea = this.$('textarea.d-editor-input');
|
||||
const insert = `${sel.pre}${text}`;
|
||||
|
@ -565,13 +616,28 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
actions: {
|
||||
emojiSelected(code) {
|
||||
let selected = this._getSelected();
|
||||
const captures = selected.pre.match(/\B:(\w*)$/);
|
||||
|
||||
if(_.isEmpty(captures)) {
|
||||
this._addText(selected, `:${code}:`);
|
||||
} else {
|
||||
let numOfRemovedChars = selected.pre.length - captures[1].length;
|
||||
selected.pre = selected.pre.slice(0, selected.pre.length - captures[1].length);
|
||||
selected.start -= numOfRemovedChars;
|
||||
selected.end -= numOfRemovedChars;
|
||||
this._addText(selected, `${code}:`);
|
||||
}
|
||||
},
|
||||
|
||||
toolbarButton(button) {
|
||||
const selected = this._getSelected(button.trimLeading);
|
||||
const toolbarEvent = {
|
||||
selected,
|
||||
selectText: (from, length) => this._selectText(from, length),
|
||||
applySurround: (head, tail, exampleKey, opts) => this._applySurround(selected, head, tail, exampleKey, opts),
|
||||
applyList: (head, exampleKey) => this._applyList(selected, head, exampleKey),
|
||||
applyList: (head, exampleKey, opts) => this._applyList(selected, head, exampleKey, opts),
|
||||
addText: text => this._addText(selected, text),
|
||||
replaceText: text => this._addText({pre: '', post: ''}, text),
|
||||
getText: () => this.get('value'),
|
||||
|
@ -643,11 +709,7 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
emoji() {
|
||||
showSelector({
|
||||
appendTo: this.$(),
|
||||
register: this.register,
|
||||
onSelect: title => this._addText(this._getSelected(), `:${title}:`)
|
||||
});
|
||||
this.set('emojiPickerIsActive', !this.get('emojiPickerIsActive'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import loadScript from "discourse/lib/load-script";
|
||||
import { default as computed, on } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Em.Component.extend({
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["date-picker-wrapper"],
|
||||
_picker: null,
|
||||
|
||||
|
|
|
@ -0,0 +1,543 @@
|
|||
import { observes } from "ember-addons/ember-computed-decorators";
|
||||
import { findRawTemplate } from "discourse/lib/raw-templates";
|
||||
import { emojiUrlFor } from "discourse/lib/text";
|
||||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import { emojis } from "pretty-text/emoji/data";
|
||||
import { extendedEmojiList, isSkinTonableEmoji } from "pretty-text/emoji";
|
||||
|
||||
const keyValueStore = new KeyValueStore("discourse_emojis_");
|
||||
const EMOJI_USAGE = "emojiUsage";
|
||||
const EMOJI_SELECTED_DIVERSITY = "emojiSelectedDiversity";
|
||||
const PER_ROW = 11;
|
||||
const customEmojis = _.map(_.keys(extendedEmojiList()), code => {
|
||||
return { code, src: emojiUrlFor(code) };
|
||||
});
|
||||
|
||||
export function resetCache() {
|
||||
keyValueStore.setObject({ key: EMOJI_USAGE, value: [] });
|
||||
keyValueStore.setObject({ key: EMOJI_SELECTED_DIVERSITY, value: 1 });
|
||||
}
|
||||
|
||||
let $picker, $filter, $results, $list, scrollPosition, $visibleSections, _checkTimeout;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
|
||||
this._unbindEvents();
|
||||
this.appEvents.off("emoji-picker:close");
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
|
||||
this.appEvents.on("emoji-picker:close", () => this.set("active", false));
|
||||
|
||||
$picker = this.$(".emoji-picker");
|
||||
|
||||
if (!keyValueStore.getObject(EMOJI_USAGE)) {
|
||||
keyValueStore.setObject({ key: EMOJI_USAGE, value: [] });
|
||||
} else if(_.isPlainObject(keyValueStore.getObject(EMOJI_USAGE))) {
|
||||
// handle legacy format
|
||||
keyValueStore.setObject({ key: EMOJI_USAGE, value: _.keys(keyValueStore.getObject(EMOJI_USAGE)) });
|
||||
}
|
||||
|
||||
scrollPosition = 0;
|
||||
},
|
||||
|
||||
didUpdateAttrs() {
|
||||
this._super();
|
||||
|
||||
if (this.get("active")) {
|
||||
this.show();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
@observes("filter")
|
||||
filterChanged() {
|
||||
$filter.find(".clear-filter").toggle(!_.isEmpty(this.get("filter")));
|
||||
Ember.run.debounce(this, this._filterEmojisList, 250);
|
||||
},
|
||||
|
||||
@observes("selectedDiversity")
|
||||
selectedDiversityChanged() {
|
||||
keyValueStore.setObject({key: EMOJI_SELECTED_DIVERSITY, value: this.get("selectedDiversity")});
|
||||
|
||||
$.each($list.find(".emoji[data-loaded='1'].diversity"), (_, button) => {
|
||||
$(button).css("background-image", "").removeAttr("data-loaded");
|
||||
});
|
||||
|
||||
if(this.get("filter") !== "") {
|
||||
$.each($results.find(".emoji.diversity"), (_, button) => this._setButtonBackground(button, true) );
|
||||
}
|
||||
|
||||
this._updateSelectedDiversity();
|
||||
},
|
||||
|
||||
@observes("recentEmojis")
|
||||
recentEmojisChanged() {
|
||||
const previousScrollTop = scrollPosition;
|
||||
const $recentSection = $list.find(".section[data-section='recent']");
|
||||
const $recentSectionGroup = $recentSection.find(".section-group");
|
||||
const $recentCategory = $picker.find(".category-icon button[data-section='recent']").parent();
|
||||
let persistScrollPosition = !$recentCategory.is(":visible") ? true : false;
|
||||
|
||||
// we set height to 0 to avoid it being taken into account for scroll position
|
||||
if(_.isEmpty(this.get("recentEmojis"))) {
|
||||
$recentCategory.hide();
|
||||
$recentSection.css("height", 0).hide();
|
||||
} else {
|
||||
$recentCategory.show();
|
||||
$recentSection.css("height", "auto").show();
|
||||
}
|
||||
|
||||
const recentEmojis = _.map(this.get("recentEmojis"), code => {
|
||||
return { code, src: emojiUrlFor(code) };
|
||||
});
|
||||
const template = findRawTemplate("emoji-picker-recent")({recentEmojis});
|
||||
$recentSectionGroup.html(template);
|
||||
|
||||
if(persistScrollPosition) {
|
||||
$list.scrollTop(previousScrollTop + $recentSection.outerHeight());
|
||||
}
|
||||
|
||||
this._bindHover($recentSectionGroup);
|
||||
},
|
||||
|
||||
close() {
|
||||
$picker
|
||||
.css({width: "", left: "", bottom: "", display: "none"})
|
||||
.empty();
|
||||
|
||||
this.$().find(".emoji-picker-modal").remove();
|
||||
|
||||
this._unbindEvents();
|
||||
|
||||
clearTimeout(_checkTimeout);
|
||||
},
|
||||
|
||||
show() {
|
||||
const template = findRawTemplate("emoji-picker")({customEmojis});
|
||||
$picker.html(template);
|
||||
this.$().append("<div class='emoji-picker-modal'></div>");
|
||||
|
||||
$filter = $picker.find(".filter");
|
||||
$results = $picker.find(".results");
|
||||
$list = $picker.find(".list");
|
||||
|
||||
this.set("selectedDiversity", keyValueStore.getObject(EMOJI_SELECTED_DIVERSITY) || 1);
|
||||
this.set("recentEmojis", keyValueStore.getObject(EMOJI_USAGE) || []);
|
||||
|
||||
this._bindEvents();
|
||||
|
||||
Ember.run.scheduleOnce("afterRender", this, function() {
|
||||
this._sectionLoadingCheck();
|
||||
this._loadCategoriesEmojis();
|
||||
this._positionPicker();
|
||||
this._scrollTo();
|
||||
this._updateSelectedDiversity();
|
||||
});
|
||||
},
|
||||
|
||||
_updateSelectedDiversity() {
|
||||
const $diversityPicker = $picker.find(".diversity-picker");
|
||||
|
||||
$diversityPicker.find(".diversity-scale").removeClass("selected");
|
||||
$diversityPicker
|
||||
.find(`.diversity-scale[data-level="${this.get("selectedDiversity")}"]`)
|
||||
.addClass("selected");
|
||||
},
|
||||
|
||||
_sectionLoadingCheck() {
|
||||
_checkTimeout = setTimeout(() => { this._sectionLoadingCheck(); }, 500);
|
||||
Ember.run.throttle(this, this._checkVisibleSection, 100);
|
||||
},
|
||||
|
||||
_loadCategoriesEmojis() {
|
||||
$.each($picker.find(".categories-column button.emoji"), (_, button) => {
|
||||
const $button = $(button);
|
||||
const code = this._codeWithDiversity($button.data("tabicon"), false);
|
||||
$button.css("background-image", `url("${emojiUrlFor(code)}")`);
|
||||
});
|
||||
},
|
||||
|
||||
_bindEvents() {
|
||||
this._bindDiversityClick();
|
||||
this._bindSectionsScroll();
|
||||
this._bindEmojiClick($list.find(".section-group"));
|
||||
this._bindClearRecentEmojisGroup();
|
||||
this._bindResizing();
|
||||
this._bindCategoryClick();
|
||||
this._bindModalClick();
|
||||
this._bindFilterInput();
|
||||
|
||||
if(!this.site.isMobileDevice) {
|
||||
this._bindHover();
|
||||
}
|
||||
},
|
||||
|
||||
_bindModalClick() {
|
||||
this.$(".emoji-picker-modal")
|
||||
.on("click", () => this.set("active", false));
|
||||
|
||||
this.$(document).on("click.emoji-picker", (event) => {
|
||||
const onPicker = $(event.target).parents(".emoji-picker").length === 1;
|
||||
const onGrippie = event.target.className.indexOf("grippie") > -1;
|
||||
if(!onPicker && !onGrippie) {
|
||||
this.set("active", false);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_unbindEvents() {
|
||||
this.$(window).off("resize");
|
||||
this.$(".emoji-picker-modal").off("click");
|
||||
Ember.$("#reply-control").off("div-resizing");
|
||||
this.$(document).off("click.emoji-picker");
|
||||
},
|
||||
|
||||
_filterEmojisList() {
|
||||
if (this.get("filter") === "") {
|
||||
$filter.find("input[name='filter']").val("");
|
||||
$results.empty().hide();
|
||||
$list.show();
|
||||
} else {
|
||||
const lowerCaseFilter = this.get("filter").toLowerCase();
|
||||
const filterableEmojis = emojis.concat(_.keys(extendedEmojiList()));
|
||||
const filteredCodes = _.filter(filterableEmojis, code => {
|
||||
return code.indexOf(lowerCaseFilter) > -1;
|
||||
}).slice(0, 30);
|
||||
$results.empty().html(
|
||||
_.map(filteredCodes, (code) => {
|
||||
const hasDiversity = isSkinTonableEmoji(code);
|
||||
const diversity = hasDiversity ? "diversity" : "";
|
||||
const scaledCode = this._codeWithDiversity(code, hasDiversity);
|
||||
return `<button style="background-image: url('${emojiUrlFor(scaledCode)}')" type="button" class="emoji ${diversity}" tabindex="-1" title="${code}"></button>`;
|
||||
})
|
||||
).show();
|
||||
this._bindHover($results);
|
||||
this._bindEmojiClick($results);
|
||||
$list.hide();
|
||||
}
|
||||
},
|
||||
|
||||
_bindFilterInput() {
|
||||
const $input = $filter.find("input");
|
||||
|
||||
$input.on("input", (event) => {
|
||||
this.set("filter", event.currentTarget.value);
|
||||
});
|
||||
|
||||
$filter.find(".clear-filter").on("click", () => {
|
||||
$input.val("").focus();
|
||||
this.set("filter", "");
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
_bindCategoryClick() {
|
||||
$picker.find(".category-icon").on("click", "button.emoji", (event) => {
|
||||
this.set("filter", "");
|
||||
$results.empty();
|
||||
$list.show();
|
||||
|
||||
const section = $(event.currentTarget).data("section");
|
||||
const $section = $list.find(`.section[data-section="${section}"]`);
|
||||
const scrollTop = $list.scrollTop() + ($section.offset().top - $list.offset().top);
|
||||
this._scrollTo(scrollTop);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
_bindHover($hoverables) {
|
||||
const replaceInfoContent = (html) => $picker.find(".footer .info").html(html || "");
|
||||
|
||||
($hoverables || $list.find(".section-group")).on({
|
||||
mouseover: (event) => {
|
||||
const code = this._codeForEmojiButton($(event.currentTarget));
|
||||
const html = `<img src="${emojiUrlFor(code)}" class="emoji"> <span>:${code}:<span>`;
|
||||
replaceInfoContent(html);
|
||||
},
|
||||
mouseleave: () => replaceInfoContent()
|
||||
}, "button.emoji");
|
||||
},
|
||||
|
||||
_bindResizing() {
|
||||
this.$(window).on("resize", () => {
|
||||
Ember.run.throttle(this, this._positionPicker, 16);
|
||||
});
|
||||
|
||||
Ember.$("#reply-control").on("div-resizing", () => {
|
||||
Ember.run.throttle(this, this._positionPicker, 16);
|
||||
});
|
||||
},
|
||||
|
||||
_bindClearRecentEmojisGroup() {
|
||||
const $recent = $picker.find(".section[data-section='recent'] .clear-recent");
|
||||
$recent.on("click", () => {
|
||||
keyValueStore.setObject({ key: EMOJI_USAGE, value: [] });
|
||||
this.set("recentEmojis", []);
|
||||
this._scrollTo(0);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
_bindEmojiClick($emojisContainer) {
|
||||
const handler = (event) => {
|
||||
const code = this._codeForEmojiButton($(event.currentTarget));
|
||||
|
||||
if($(event.currentTarget).parents(".section[data-section='recent']").length === 0) {
|
||||
this._trackEmojiUsage(code);
|
||||
}
|
||||
|
||||
this.sendAction("emojiSelected", code);
|
||||
|
||||
if(this.$(".emoji-picker-modal").hasClass("fadeIn")) {
|
||||
this.set("active", false);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if(this.site.isMobileDevice) {
|
||||
const self = this;
|
||||
|
||||
$emojisContainer
|
||||
.off("touchstart")
|
||||
.on("touchstart", "button.emoji", (touchStartEvent) => {
|
||||
const $this = $(touchStartEvent.currentTarget);
|
||||
$this.on("touchend", (touchEndEvent) => {
|
||||
handler.bind(self)(touchEndEvent);
|
||||
$this.off("touchend");
|
||||
});
|
||||
$this.on("touchmove", () => $this.off("touchend") );
|
||||
});
|
||||
} else {
|
||||
$emojisContainer.off("click").on("click", "button.emoji", e => handler.bind(this)(e) );
|
||||
}
|
||||
},
|
||||
|
||||
_bindSectionsScroll() {
|
||||
$list.on("scroll", () => {
|
||||
scrollPosition = $list.scrollTop();
|
||||
Ember.run.throttle(this, this._checkVisibleSection, 150);
|
||||
});
|
||||
},
|
||||
|
||||
_checkVisibleSection() {
|
||||
// make sure we stop loading if picker has been removed
|
||||
if(!$picker) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $sections = $list.find(".section");
|
||||
const listHeight = $list.innerHeight();
|
||||
let $selectedSection;
|
||||
|
||||
$visibleSections = _.filter($sections, section => {
|
||||
const $section = $(section);
|
||||
const sectionTop = $section.position().top;
|
||||
return sectionTop + $section.height() > 0 && sectionTop < listHeight;
|
||||
});
|
||||
|
||||
if (!_.isEmpty(this.get("recentEmojis")) && scrollPosition === 0) {
|
||||
$selectedSection = $(_.first($visibleSections));
|
||||
} else {
|
||||
$selectedSection = $(_.last($visibleSections));
|
||||
}
|
||||
|
||||
if($selectedSection) {
|
||||
$picker.find(".category-icon").removeClass("current");
|
||||
$picker.find(`.category-icon button[data-section='${$selectedSection.data("section")}']`)
|
||||
.parent()
|
||||
.addClass("current");
|
||||
|
||||
this._loadVisibleSections();
|
||||
}
|
||||
},
|
||||
|
||||
_loadVisibleSections() {
|
||||
if(!$visibleSections) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listHeight = $list.innerHeight();
|
||||
$visibleSections.forEach(visibleSection => {
|
||||
const $unloadedEmojis = $(visibleSection).find("button.emoji[data-loaded!='1']");
|
||||
$.each($unloadedEmojis, (_, button) => {
|
||||
const $button = $(button);
|
||||
const buttonTop = $button.position().top;
|
||||
const buttonHeight = $button.height();
|
||||
|
||||
if(buttonTop + buttonHeight > 0 && buttonTop - buttonHeight < listHeight) {
|
||||
this._setButtonBackground($button);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_bindDiversityClick() {
|
||||
const $diversityScales = $picker.find(".diversity-picker .diversity-scale");
|
||||
$diversityScales.on("click", (event) => {
|
||||
const $selectedDiversity = $(event.currentTarget);
|
||||
this.set("selectedDiversity", parseInt($selectedDiversity.data("level")));
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
_isReplyControlExpanded() {
|
||||
const verticalSpace = this.$(window).height() -
|
||||
Ember.$(".d-header").height() -
|
||||
Ember.$("#reply-control").height();
|
||||
|
||||
return verticalSpace < $picker.height() - 48;
|
||||
},
|
||||
|
||||
_positionPicker(){
|
||||
if(!this.get("active")) { return; }
|
||||
|
||||
let windowWidth = this.$(window).width();
|
||||
|
||||
const desktopModalePositioning = options => {
|
||||
let attributes = {
|
||||
width: Math.min(windowWidth, 400) - 12,
|
||||
marginLeft: -(Math.min(windowWidth, 400)/2) + 6,
|
||||
marginTop: -130,
|
||||
left: "50%",
|
||||
bottom: "",
|
||||
top: "50%",
|
||||
display: "flex"
|
||||
};
|
||||
|
||||
this.$(".emoji-picker-modal").addClass("fadeIn");
|
||||
$picker.css(_.merge(attributes, options));
|
||||
};
|
||||
|
||||
const mobilePositioning = options => {
|
||||
let attributes = {
|
||||
width: windowWidth - 12,
|
||||
marginLeft: 5,
|
||||
marginTop: -130,
|
||||
left: 0,
|
||||
bottom: "",
|
||||
top: "50%",
|
||||
display: "flex"
|
||||
};
|
||||
|
||||
this.$(".emoji-picker-modal").addClass("fadeIn");
|
||||
$picker.css(_.merge(attributes, options));
|
||||
};
|
||||
|
||||
const desktopPositioning = options => {
|
||||
let attributes = {
|
||||
width: windowWidth < 485 ? windowWidth - 12 : 400,
|
||||
marginLeft: "",
|
||||
marginTop: "",
|
||||
right: "",
|
||||
left: "",
|
||||
bottom: 32,
|
||||
top: "",
|
||||
display:
|
||||
"flex"
|
||||
};
|
||||
|
||||
this.$(".emoji-picker-modal").removeClass("fadeIn");
|
||||
$picker.css(_.merge(attributes, options));
|
||||
};
|
||||
|
||||
if(Ember.testing) {
|
||||
desktopPositioning();
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.site.isMobileDevice) {
|
||||
mobilePositioning();
|
||||
} else {
|
||||
if(this._isReplyControlExpanded()) {
|
||||
let $editorWrapper = Ember.$(".d-editor-preview-wrapper");
|
||||
if(($editorWrapper.is(":visible") && $editorWrapper.width() < 400) || windowWidth < 485) {
|
||||
desktopModalePositioning();
|
||||
} else {
|
||||
if($editorWrapper.is(":visible")) {
|
||||
let previewOffset = Ember.$(".d-editor-preview-wrapper").offset();
|
||||
let replyControlOffset = Ember.$("#reply-control").offset();
|
||||
let left = previewOffset.left - replyControlOffset.left;
|
||||
desktopPositioning({left});
|
||||
} else {
|
||||
desktopPositioning({
|
||||
right: (Ember.$("#reply-control").width() - Ember.$(".d-editor-container").width()) / 2
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(windowWidth < 485) {
|
||||
desktopModalePositioning();
|
||||
} else {
|
||||
let previewInputOffset = Ember.$(".d-editor-input").offset();
|
||||
let replyControlOffset = Ember.$("#reply-control").offset() || {left: 0};
|
||||
let left = previewInputOffset.left - replyControlOffset.left;
|
||||
desktopPositioning({left, bottom: Ember.$("#reply-control").height() - 48});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const infoMaxWidth = $picker.width() -
|
||||
$picker.find(".categories-column").width() -
|
||||
$picker.find(".diversity-picker").width() -
|
||||
32;
|
||||
$picker.find(".info").css("max-width", infoMaxWidth);
|
||||
},
|
||||
|
||||
_codeWithDiversity(code, diversity) {
|
||||
if(diversity && this.get("selectedDiversity") !== 1) {
|
||||
return `${code}:t${this.get("selectedDiversity")}`;
|
||||
} else {
|
||||
return code;
|
||||
}
|
||||
},
|
||||
|
||||
_trackEmojiUsage(code) {
|
||||
let recent = keyValueStore.getObject(EMOJI_USAGE) || [];
|
||||
recent = recent.filter(r => r !== code);
|
||||
recent.unshift(code);
|
||||
recent.length = Math.min(recent.length, PER_ROW);
|
||||
keyValueStore.setObject({ key: EMOJI_USAGE, value: recent });
|
||||
this.set("recentEmojis", recent);
|
||||
},
|
||||
|
||||
_scrollTo(y) {
|
||||
const yPosition = _.isUndefined(y) ? scrollPosition : y;
|
||||
|
||||
$list.scrollTop(yPosition);
|
||||
|
||||
// if we don’t actually scroll we need to force it
|
||||
if(yPosition === 0) {
|
||||
$list.scroll();
|
||||
}
|
||||
},
|
||||
|
||||
_codeForEmojiButton($button) {
|
||||
const title = $button.attr("title");
|
||||
return this._codeWithDiversity(title, $button.hasClass("diversity"));
|
||||
},
|
||||
|
||||
_setButtonBackground(button, diversity) {
|
||||
const $button = $(button);
|
||||
const code = this._codeWithDiversity(
|
||||
$button.attr("title"),
|
||||
diversity || $button.hasClass("diversity")
|
||||
);
|
||||
|
||||
// force visual reloading if needed
|
||||
if($button.css("background-image") !== "none") {
|
||||
$button.css("background-image", "");
|
||||
}
|
||||
|
||||
$button
|
||||
.attr("data-loaded", 1)
|
||||
.css("background-image", `url("${emojiUrlFor(code)}")`);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
|
||||
actions: {
|
||||
expandItem() {
|
||||
const item = this.get('item');
|
||||
const topicId = item.get('topic_id');
|
||||
const postNumber = item.get('post_number');
|
||||
|
||||
return ajax(`/posts/by_number/${topicId}/${postNumber}.json`).then(result => {
|
||||
item.set('truncated', false);
|
||||
item.set('excerpt', result.cooked);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'button',
|
||||
classNames: ['btn-flat'],
|
||||
attributeBindings: ['disabled', 'translatedTitle:title'],
|
||||
|
||||
@computed("title")
|
||||
translatedTitle(title) {
|
||||
if (title) return I18n.t(title);
|
||||
},
|
||||
|
||||
click() {
|
||||
return this.attrs.action();
|
||||
}
|
||||
});
|
|
@ -1,8 +1,10 @@
|
|||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import Group from 'discourse/models/group';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
loading: false,
|
||||
|
||||
@computed("model.public")
|
||||
canJoinGroup(publicGroup) {
|
||||
return publicGroup;
|
||||
|
@ -17,22 +19,6 @@ export default Ember.Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@computed
|
||||
disableRequestMembership() {
|
||||
if (this.currentUser) {
|
||||
return this.currentUser.trust_level < this.siteSettings.min_trust_to_send_messages;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("disableRequestMembership")
|
||||
requestMembershipButtonTitle(disableRequestMembership) {
|
||||
if (disableRequestMembership) {
|
||||
return "groups.request_membership_pm.disabled";
|
||||
}
|
||||
},
|
||||
|
||||
_showLoginModal() {
|
||||
this.sendAction('showLogin');
|
||||
$.cookie('destination_url', window.location.href);
|
||||
|
@ -67,13 +53,12 @@ export default Ember.Component.extend({
|
|||
|
||||
requestMembership() {
|
||||
if (this.currentUser) {
|
||||
const groupName = this.get('model.name');
|
||||
this.set('loading', true);
|
||||
|
||||
Group.loadOwners(groupName).then(result => {
|
||||
const names = result.map(owner => owner.username).join(",");
|
||||
const title = I18n.t('groups.request_membership_pm.title');
|
||||
const body = I18n.t('groups.request_membership_pm.body', { groupName });
|
||||
this.sendAction("createNewMessageViaParams", names, title, body);
|
||||
this.get('model').requestMembership().then(result => {
|
||||
DiscourseURL.routeTo(result.relative_url);
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
} else {
|
||||
this._showLoginModal();
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
export default Ember.Component.extend({
|
||||
actions: {
|
||||
// TODO: When on Ember 1.13, use a closure action
|
||||
loadMore() {
|
||||
this.sendAction('loadMore');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -15,31 +15,27 @@ export default Ember.Component.extend({
|
|||
|
||||
@on('didInsertElement')
|
||||
_initializeAutocomplete(opts) {
|
||||
var self = this;
|
||||
var selectedGroups;
|
||||
var groupNames = this.get('groupNames');
|
||||
let selectedGroups;
|
||||
let groupNames = this.get('groupNames');
|
||||
|
||||
self.$('input').autocomplete({
|
||||
this.$('input').autocomplete({
|
||||
allowAny: false,
|
||||
items: _.isArray(groupNames) ? groupNames : (Ember.isEmpty(groupNames)) ? [] : [groupNames],
|
||||
single: this.get('single'),
|
||||
updateData: (opts && opts.updateData) ? opts.updateData : false,
|
||||
onChangeItems: function(items){
|
||||
onChangeItems: items => {
|
||||
selectedGroups = items;
|
||||
self.set("groupNames", items.join(","));
|
||||
this.set("groupNames", items.join(","));
|
||||
},
|
||||
transformComplete: function(g) {
|
||||
transformComplete: g => {
|
||||
return g.name;
|
||||
},
|
||||
dataSource: function(term) {
|
||||
return self.get("groupFinder")(term).then(function(groups){
|
||||
dataSource: term => {
|
||||
return this.get("groupFinder")(term).then(groups => {
|
||||
if(!selectedGroups) return groups;
|
||||
|
||||
if(!selectedGroups){
|
||||
return groups;
|
||||
}
|
||||
|
||||
return groups.filter(function(group){
|
||||
return !selectedGroups.any(function(s){return s === group.name;});
|
||||
return groups.filter(group => {
|
||||
return !selectedGroups.any(s => s === group.name);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import highlightText from 'discourse/lib/highlight-text';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'span',
|
||||
|
||||
_highlightOnInsert: function() {
|
||||
const term = this.get('highlight');
|
||||
const self = this;
|
||||
|
||||
if(!_.isEmpty(term)) {
|
||||
self.$().highlight(term.split(/\s+/), {className: 'search-highlight'});
|
||||
}
|
||||
highlightText(this.$(), term);
|
||||
}.observes('highlight').on('didInsertElement')
|
||||
|
||||
});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { keyDirty } from 'discourse/widgets/widget';
|
||||
import { diff, patch } from 'virtual-dom';
|
||||
import { WidgetClickHook } from 'discourse/widgets/hooks';
|
||||
import { renderedKey, queryRegistry } from 'discourse/widgets/widget';
|
||||
import { queryRegistry } from 'discourse/widgets/widget';
|
||||
import { getRegister } from 'discourse-common/lib/get-owner';
|
||||
import DirtyKeys from 'discourse/lib/dirty-keys';
|
||||
|
||||
const _cleanCallbacks = {};
|
||||
export function addWidgetCleanCallback(widgetName, fn) {
|
||||
|
@ -18,6 +18,7 @@ export default Ember.Component.extend({
|
|||
_renderCallback: null,
|
||||
_childEvents: null,
|
||||
_dispatched: null,
|
||||
dirtyKeys: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
|
@ -34,6 +35,7 @@ export default Ember.Component.extend({
|
|||
this._childEvents = [];
|
||||
this._connected = [];
|
||||
this._dispatched = [];
|
||||
this.dirtyKeys = new DirtyKeys(name);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
|
@ -73,7 +75,7 @@ export default Ember.Component.extend({
|
|||
|
||||
eventDispatched(eventName, key, refreshArg) {
|
||||
const onRefresh = Ember.String.camelize(eventName.replace(/:/, '-'));
|
||||
keyDirty(key, { onRefresh, refreshArg });
|
||||
this.dirtyKeys.keyDirty(key, { onRefresh, refreshArg });
|
||||
this.queueRerender();
|
||||
},
|
||||
|
||||
|
@ -104,7 +106,10 @@ export default Ember.Component.extend({
|
|||
|
||||
const t0 = new Date().getTime();
|
||||
const args = this.get('args') || this.buildArgs();
|
||||
const opts = { model: this.get('model') };
|
||||
const opts = {
|
||||
model: this.get('model'),
|
||||
dirtyKeys: this.dirtyKeys,
|
||||
};
|
||||
const newTree = new this._widgetClass(args, this.register, opts);
|
||||
|
||||
newTree._rerenderable = this;
|
||||
|
@ -122,8 +127,8 @@ export default Ember.Component.extend({
|
|||
this._renderCallback = null;
|
||||
}
|
||||
this.afterRender();
|
||||
this.dirtyKeys.renderedKey('*');
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', () => renderedKey('*'));
|
||||
if (this.profileWidget) {
|
||||
console.log(new Date().getTime() - t0);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { keyDirty } from 'discourse/widgets/widget';
|
||||
import MountWidget from 'discourse/components/mount-widget';
|
||||
import { cloak, uncloak } from 'discourse/widgets/post-stream';
|
||||
import { isWorkaroundActive } from 'discourse/lib/safari-hacks';
|
||||
|
@ -245,13 +244,13 @@ export default MountWidget.extend({
|
|||
this.appEvents.on('post-stream:refresh', args => {
|
||||
if (args) {
|
||||
if (args.id) {
|
||||
keyDirty(`post-${args.id}`);
|
||||
this.dirtyKeys.keyDirty(`post-${args.id}`);
|
||||
|
||||
if (args.refreshLikes) {
|
||||
keyDirty(`post-menu-${args.id}`, { onRefresh: 'refreshLikes' });
|
||||
this.dirtyKeys.keyDirty(`post-menu-${args.id}`, { onRefresh: 'refreshLikes' });
|
||||
}
|
||||
} else if (args.force) {
|
||||
keyDirty(`*`);
|
||||
this.dirtyKeys.forceAll();
|
||||
}
|
||||
}
|
||||
this.queueRerender();
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
import Group from 'discourse/models/group';
|
||||
import Badge from 'discourse/models/badge';
|
||||
|
||||
const REGEXP_BLOCKS = /(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/g;
|
||||
|
||||
|
@ -77,7 +79,8 @@ export default Em.Component.extend({
|
|||
likes: false,
|
||||
private: false,
|
||||
seen: false
|
||||
}
|
||||
},
|
||||
all_tags: false
|
||||
},
|
||||
status: '',
|
||||
min_post_count: '',
|
||||
|
@ -230,13 +233,15 @@ export default Em.Component.extend({
|
|||
|
||||
const match = this.filterBlocks(REGEXP_TAGS_PREFIX);
|
||||
const tags = this.get('searchedTerms.tags');
|
||||
const contain_all_tags = this.get('searchedTerms.special.all_tags');
|
||||
|
||||
if (match.length !== 0) {
|
||||
const existingInput = _.isArray(tags) ? tags.join(',') : tags;
|
||||
const join_char = contain_all_tags ? '+' : ',';
|
||||
const existingInput = _.isArray(tags) ? tags.join(join_char) : tags;
|
||||
const userInput = match[0].replace(REGEXP_TAGS_REPLACE, '');
|
||||
|
||||
if (existingInput !== userInput) {
|
||||
this.set('searchedTerms.tags', (userInput.length !== 0) ? userInput.split(',') : []);
|
||||
this.set('searchedTerms.tags', (userInput.length !== 0) ? userInput.split(join_char) : []);
|
||||
}
|
||||
} else if (tags.length !== 0) {
|
||||
this.set('searchedTerms.tags', []);
|
||||
|
@ -365,14 +370,16 @@ export default Em.Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@observes('searchedTerms.tags')
|
||||
@observes('searchedTerms.tags', 'searchedTerms.special.all_tags')
|
||||
updateSearchTermForTags() {
|
||||
const match = this.filterBlocks(REGEXP_TAGS_PREFIX);
|
||||
const tagFilter = this.get('searchedTerms.tags');
|
||||
let searchTerm = this.get('searchTerm') || '';
|
||||
const contain_all_tags = this.get('searchedTerms.special.all_tags');
|
||||
|
||||
if (tagFilter && tagFilter.length !== 0) {
|
||||
const tags = tagFilter.join(',');
|
||||
const join_char = contain_all_tags ? '+' : ',';
|
||||
const tags = tagFilter.join(join_char);
|
||||
|
||||
if (match.length !== 0) {
|
||||
searchTerm = searchTerm.replace(match[0], `tags:${tags}`);
|
||||
|
@ -520,12 +527,10 @@ export default Em.Component.extend({
|
|||
},
|
||||
|
||||
groupFinder(term) {
|
||||
const Group = require('discourse/models/group').default;
|
||||
return Group.findAll({search: term, ignore_automatic: false});
|
||||
return Group.findAll({ term: term, ignore_automatic: false });
|
||||
},
|
||||
|
||||
badgeFinder(term) {
|
||||
const Badge = require('discourse/models/badge').default;
|
||||
return Badge.findAll({search: term});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,13 +2,7 @@ import { propertyEqual } from 'discourse/lib/computed';
|
|||
import { actionDescription } from "discourse/components/small-action";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [":item", "item.hidden", "item.deleted", "moderatorAction"],
|
||||
classNameBindings: [":item", "item.hidden", "item.deleted:deleted", "moderatorAction"],
|
||||
moderatorAction: propertyEqual("item.post_type", "site.post_types.moderator_action"),
|
||||
actionDescription: actionDescription("item.action_code", "item.created_at", "item.username"),
|
||||
|
||||
actions: {
|
||||
removeBookmark(userAction) {
|
||||
this.sendAction("removeBookmark", userAction);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import MountWidget from 'discourse/components/mount-widget';
|
||||
|
||||
export default MountWidget.extend({
|
||||
classNames: 'topic-admin-menu-button-container',
|
||||
tagName: 'span',
|
||||
widget: "topic-admin-menu-button",
|
||||
|
||||
buildArgs() {
|
||||
return this.getProperties('topic', 'fixed', 'openUpwards');
|
||||
return this.getProperties('topic', 'fixed', 'openUpwards', 'rightSide');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -92,17 +92,18 @@ export default Ember.Component.extend(CleansUp, {
|
|||
this.appEvents.off('topic-entrance:show');
|
||||
},
|
||||
|
||||
_jumpTo(destination) {
|
||||
this.cleanUp();
|
||||
DiscourseURL.routeTo(destination);
|
||||
},
|
||||
|
||||
actions: {
|
||||
enterTop() {
|
||||
const topic = this.get('topic');
|
||||
this.appEvents.trigger('header:update-topic', topic);
|
||||
DiscourseURL.routeTo(topic.get('url'));
|
||||
this._jumpTo(this.get('topic.url'));
|
||||
},
|
||||
|
||||
enterBottom() {
|
||||
const topic = this.get('topic');
|
||||
this.appEvents.trigger('header:update-topic', topic);
|
||||
DiscourseURL.routeTo(topic.get('lastPostUrl'));
|
||||
this._jumpTo(this.get('topic.lastPostUrl'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ export default Ember.Component.extend({
|
|||
|
||||
@computed('postStream.loaded', 'topic.currentPost', 'postStream.filteredPostsCount')
|
||||
hideProgress(loaded, currentPost, filteredPostsCount) {
|
||||
return (!loaded) || (!currentPost) || (filteredPostsCount < 2);
|
||||
return (!loaded) || (!currentPost) || (!this.site.mobileView && filteredPostsCount < 2);
|
||||
},
|
||||
|
||||
@computed('postStream.filteredPostsCount')
|
||||
|
@ -52,8 +52,14 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
_topicScrolled(event) {
|
||||
this.set('progressPosition', event.postIndex);
|
||||
this._streamPercentage = event.percent;
|
||||
if (this.get('docked')) {
|
||||
this.set('progressPosition', this.get('postStream.filteredPostsCount'));
|
||||
this._streamPercentage = 1.0;
|
||||
} else {
|
||||
this.set('progressPosition', event.postIndex);
|
||||
this._streamPercentage = event.percent;
|
||||
}
|
||||
|
||||
this._updateBar();
|
||||
},
|
||||
|
||||
|
@ -110,11 +116,10 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
_dock() {
|
||||
const maximumOffset = $('#topic-footer-buttons').offset(),
|
||||
const maximumOffset = $('#topic-bottom').offset(),
|
||||
composerHeight = $('#reply-control').height() || 0,
|
||||
$topicProgressWrapper = this.$(),
|
||||
offset = window.pageYOffset || $('html').scrollTop(),
|
||||
topicProgressHeight = $('#topic-progress').height();
|
||||
offset = window.pageYOffset || $('html').scrollTop();
|
||||
|
||||
if (!$topicProgressWrapper || $topicProgressWrapper.length === 0) {
|
||||
return;
|
||||
|
@ -124,7 +129,13 @@ export default Ember.Component.extend({
|
|||
if (maximumOffset) {
|
||||
const threshold = maximumOffset.top;
|
||||
const windowHeight = $(window).height();
|
||||
isDocked = offset >= threshold - windowHeight + topicProgressHeight + composerHeight;
|
||||
const headerHeight = $('header').outerHeight(true);
|
||||
|
||||
if (this.capabilities.isIOS) {
|
||||
isDocked = offset >= (threshold - windowHeight - headerHeight + composerHeight);
|
||||
} else {
|
||||
isDocked = offset >= (threshold - windowHeight + composerHeight);
|
||||
}
|
||||
}
|
||||
|
||||
const dockPos = $(document).height() - $('#topic-bottom').offset().top;
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import KeyEnterEscape from 'discourse/mixins/key-enter-escape';
|
||||
|
||||
export default Ember.Component.extend(KeyEnterEscape, {
|
||||
elementId: 'topic-title',
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
import LoadMore from "discourse/mixins/load-more";
|
||||
import ClickTrack from 'discourse/lib/click-track';
|
||||
import { selectedText } from 'discourse/lib/utilities';
|
||||
import Post from 'discourse/models/post';
|
||||
|
||||
export default Ember.Component.extend(LoadMore, {
|
||||
loading: false,
|
||||
|
@ -44,6 +45,13 @@ export default Ember.Component.extend(LoadMore, {
|
|||
}.on('willDestroyElement'),
|
||||
|
||||
actions: {
|
||||
removeBookmark(userAction) {
|
||||
const stream = this.get('stream');
|
||||
Post.updateBookmark(userAction.get("post_id"), false).then(() => {
|
||||
stream.remove(userAction);
|
||||
});
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
if (this.get('loading')) { return; }
|
||||
|
||||
|
|
|
@ -277,7 +277,17 @@ export default Ember.Controller.extend({
|
|||
|
||||
// Toggle the reply view
|
||||
toggle() {
|
||||
this.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() {
|
||||
|
@ -330,6 +340,11 @@ export default Ember.Controller.extend({
|
|||
},
|
||||
|
||||
hitEsc() {
|
||||
if (Ember.$(".emoji-picker-modal").length === 1) {
|
||||
this.appEvents.trigger('emoji-picker:close');
|
||||
return;
|
||||
}
|
||||
|
||||
if ((this.get('messageCount') || 0) > 0) {
|
||||
this.appEvents.trigger('composer-messages:close');
|
||||
return;
|
||||
|
@ -385,20 +400,6 @@ export default Ember.Controller.extend({
|
|||
return Discourse.Category.list();
|
||||
}.property(),
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
disableSubmit: Ember.computed.or("model.loading", "isUploading"),
|
||||
|
||||
save(force) {
|
||||
|
|
|
@ -7,9 +7,10 @@ import InputValidation from 'discourse/models/input-validation';
|
|||
import PasswordValidation from "discourse/mixins/password-validation";
|
||||
import UsernameValidation from "discourse/mixins/username-validation";
|
||||
import NameValidation from "discourse/mixins/name-validation";
|
||||
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, UsernameValidation, NameValidation, {
|
||||
export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, UsernameValidation, NameValidation, UserFieldsValidation, {
|
||||
login: Ember.inject.controller(),
|
||||
|
||||
complete: false,
|
||||
|
@ -50,19 +51,10 @@ export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, U
|
|||
if (this.get('emailValidation.failed')) return true;
|
||||
if (this.get('usernameValidation.failed')) return true;
|
||||
if (this.get('passwordValidation.failed')) return true;
|
||||
if (this.get('userFieldsValidation.failed')) return true;
|
||||
|
||||
// Validate required fields
|
||||
let userFields = this.get('userFields');
|
||||
if (userFields) { userFields = userFields.filterBy('field.required'); }
|
||||
if (!Ember.isEmpty(userFields)) {
|
||||
const anyEmpty = userFields.any(function(uf) {
|
||||
const val = uf.get('value');
|
||||
return !val || Ember.isEmpty(val);
|
||||
});
|
||||
if (anyEmpty) { return true; }
|
||||
}
|
||||
return false;
|
||||
}.property('passwordRequired', 'nameValidation.failed', 'emailValidation.failed', 'usernameValidation.failed', 'passwordValidation.failed', 'formSubmitted', 'userFields.@each.value'),
|
||||
}.property('passwordRequired', 'nameValidation.failed', 'emailValidation.failed', 'usernameValidation.failed', 'passwordValidation.failed', 'userFieldsValidation.failed', 'formSubmitted'),
|
||||
|
||||
|
||||
usernameRequired: Ember.computed.not('authOptions.omit_username'),
|
||||
|
@ -82,10 +74,6 @@ export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, U
|
|||
});
|
||||
}.property(),
|
||||
|
||||
nameInstructions: function() {
|
||||
return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
|
||||
}.property(),
|
||||
|
||||
// Check the email address
|
||||
emailValidation: function() {
|
||||
// If blank, fail without a reason
|
||||
|
@ -212,18 +200,6 @@ export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, U
|
|||
return self.flash(I18n.t('create_account.failed'), 'error');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_createUserFields: function() {
|
||||
if (!this.site) { return; }
|
||||
|
||||
let userFields = this.site.get('user_fields');
|
||||
if (userFields) {
|
||||
userFields = _.sortBy(userFields, 'position').map(function(f) {
|
||||
return Ember.Object.create({ value: null, field: f });
|
||||
});
|
||||
}
|
||||
this.set('userFields', userFields);
|
||||
}.on('init')
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import BulkTopicSelection from 'discourse/mixins/bulk-topic-selection';
|
|||
import { endWith } from 'discourse/lib/computed';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
import TopicList from 'discourse/models/topic-list';
|
||||
|
||||
const controllerOpts = {
|
||||
discovery: Ember.inject.controller(),
|
||||
|
@ -60,7 +61,6 @@ const controllerOpts = {
|
|||
|
||||
this.topicTrackingState.resetTracking();
|
||||
this.store.findFiltered('topicList', {filter}).then(list => {
|
||||
const TopicList = require('discourse/models/topic-list').default;
|
||||
TopicList.hideUniformCategory(list, this.get('category'));
|
||||
|
||||
this.setProperties({ model: list });
|
||||
|
|
|
@ -5,7 +5,7 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
|
|||
|
||||
export const CLOSE_STATUS_TYPE = 'close';
|
||||
const OPEN_STATUS_TYPE = 'open';
|
||||
const PUBLISH_TO_CATEGORY_STATUS_TYPE = 'publish_to_category';
|
||||
export const PUBLISH_TO_CATEGORY_STATUS_TYPE = 'publish_to_category';
|
||||
const DELETE_STATUS_TYPE = 'delete';
|
||||
const REMINDER_TYPE = 'reminder';
|
||||
|
||||
|
@ -33,9 +33,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
];
|
||||
},
|
||||
|
||||
@computed('updateTime', 'loading')
|
||||
saveDisabled(updateTime, loading) {
|
||||
return Ember.isEmpty(updateTime) || loading;
|
||||
@computed('updateTime', 'loading', 'publishToCategory', 'topicTimer.category_id')
|
||||
saveDisabled(updateTime, loading, publishToCategory, topicTimerCategoryId) {
|
||||
return Ember.isEmpty(updateTime) ||
|
||||
loading ||
|
||||
(publishToCategory && !topicTimerCategoryId);
|
||||
},
|
||||
|
||||
@computed("model.visible")
|
||||
|
@ -70,7 +72,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
time,
|
||||
this.get('topicTimer.based_on_last_post'),
|
||||
statusType,
|
||||
this.get('categoryId')
|
||||
this.get('topicTimer.category_id')
|
||||
).then(result => {
|
||||
if (time) {
|
||||
this.send('closeModal');
|
||||
|
|
|
@ -158,7 +158,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
fetchUserDetails() {
|
||||
if (Discourse.User.currentProp('staff') && this.get('model.username')) {
|
||||
const AdminUser = require('admin/models/admin-user').default;
|
||||
const AdminUser = requirejs('admin/models/admin-user').default;
|
||||
AdminUser.find(this.get('model.user_id')).then(user => this.set('userDetails', user));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import { extractError } from 'discourse/lib/ajax-error';
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
offerHelp: null,
|
||||
helpSeen: false,
|
||||
|
||||
@computed('accountEmailOrUsername', 'disabled')
|
||||
submitDisabled(accountEmailOrUsername, disabled) {
|
||||
|
@ -35,8 +37,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
if (data.user_found === true) {
|
||||
key += '_found';
|
||||
this.set('accountEmailOrUsername', '');
|
||||
bootbox.alert(I18n.t(key, {email: escaped, username: escaped}));
|
||||
this.send("closeModal");
|
||||
this.set('offerHelp', I18n.t(key, {email: escaped, username: escaped}));
|
||||
} else {
|
||||
if (data.user_found === false) {
|
||||
key += '_not_found';
|
||||
|
@ -52,6 +53,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
ok() {
|
||||
this.send('closeModal');
|
||||
},
|
||||
|
||||
help() {
|
||||
this.setProperties({ offerHelp: I18n.t('forgot_password.help'), helpSeen: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,14 @@ export default Ember.Controller.extend({
|
|||
return Em.isEmpty(q);
|
||||
},
|
||||
|
||||
|
||||
@computed('q')
|
||||
highlightQuery(q) {
|
||||
if (!q) { return; }
|
||||
// remove l which can be used for sorting
|
||||
return _.reject(q.split(/\s+/), t => t === 'l').join(' ');
|
||||
},
|
||||
|
||||
@computed('skip_context', 'context')
|
||||
searchContextEnabled: {
|
||||
get(skip,context){
|
||||
|
@ -186,6 +194,11 @@ export default Ember.Controller.extend({
|
|||
|
||||
ajax("/search", { data: args }).then(results => {
|
||||
const model = translateResults(results) || {};
|
||||
|
||||
if (results.grouped_search_result) {
|
||||
this.set('q', results.grouped_search_result.term);
|
||||
}
|
||||
|
||||
setTransient('lastSearch', { searchKey, model }, 5);
|
||||
this.set("model", model);
|
||||
}).finally(() => this.set("searching", false));
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { emailValid } from 'discourse/lib/utilities';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import Group from 'discourse/models/group';
|
||||
import Invite from 'discourse/models/invite';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
userInvitedShow: Ember.inject.controller('user-invited-show'),
|
||||
|
@ -11,6 +13,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
hasCustomMessage: false,
|
||||
customMessage: null,
|
||||
inviteIcon: "envelope",
|
||||
invitingExistingUserToTopic: false,
|
||||
|
||||
@computed('isMessage', 'invitingToTopic')
|
||||
title(isMessage, invitingToTopic) {
|
||||
|
@ -23,9 +26,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
}
|
||||
},
|
||||
|
||||
isAdmin: function(){
|
||||
return Discourse.User.currentProp("admin");
|
||||
}.property(),
|
||||
@computed
|
||||
isAdmin() {
|
||||
return this.currentUser.admin;
|
||||
},
|
||||
|
||||
@computed('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving', 'model.details.can_invite_to')
|
||||
disabled(isAdmin, emailOrUsername, invitingToTopic, isPrivateTopic, groupNames, saving, can_invite_to) {
|
||||
|
@ -44,29 +48,32 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
return false;
|
||||
},
|
||||
|
||||
disabledCopyLink: function() {
|
||||
if (this.get('hasCustomMessage')) return true;
|
||||
if (this.get('model.saving')) return true;
|
||||
if (Ember.isEmpty(this.get('emailOrUsername'))) return true;
|
||||
const emailOrUsername = this.get('emailOrUsername').trim();
|
||||
@computed('isAdmin', 'emailOrUsername', 'model.saving', 'isPrivateTopic', 'model.groupNames', 'hasCustomMessage')
|
||||
disabledCopyLink(isAdmin, emailOrUsername, saving, isPrivateTopic, groupNames, hasCustomMessage) {
|
||||
if (hasCustomMessage) return true;
|
||||
if (saving) return true;
|
||||
if (Ember.isEmpty(emailOrUsername)) return true;
|
||||
const email = emailOrUsername.trim();
|
||||
// email must be valid
|
||||
if (!emailValid(emailOrUsername)) return true;
|
||||
if (!emailValid(email)) return true;
|
||||
// normal users (not admin) can't invite users to private topic via email
|
||||
if (!this.get('isAdmin') && this.get('isPrivateTopic') && emailValid(emailOrUsername)) return true;
|
||||
if (!isAdmin && isPrivateTopic && emailValid(email)) return true;
|
||||
// when inviting to private topic via email, group name must be specified
|
||||
if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && emailValid(emailOrUsername)) return true;
|
||||
if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(email)) return true;
|
||||
return false;
|
||||
}.property('emailOrUsername', 'model.saving', 'isPrivateTopic', 'model.groupNames', 'hasCustomMessage'),
|
||||
},
|
||||
|
||||
buttonTitle: function() {
|
||||
return this.get('model.saving') ? 'topic.inviting' : 'topic.invite_reply.action';
|
||||
}.property('model.saving'),
|
||||
@computed('model.saving')
|
||||
buttonTitle(saving) {
|
||||
return saving ? 'topic.inviting' : 'topic.invite_reply.action';
|
||||
},
|
||||
|
||||
// We are inviting to a topic if the model isn't the current user.
|
||||
// The current user would mean we are inviting to the forum in general.
|
||||
invitingToTopic: function() {
|
||||
return this.get('model') !== this.currentUser;
|
||||
}.property('model'),
|
||||
@computed('model')
|
||||
invitingToTopic(model) {
|
||||
return model !== this.currentUser;
|
||||
},
|
||||
|
||||
@computed('model', 'model.details.can_invite_via_email')
|
||||
canInviteViaEmail(model, can_invite_via_email) {
|
||||
|
@ -89,14 +96,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
isMessage: Em.computed.equal('model.archetype', 'private_message'),
|
||||
|
||||
// Allow Existing Members? (username autocomplete)
|
||||
allowExistingMembers: function() {
|
||||
return this.get('invitingToTopic');
|
||||
}.property('invitingToTopic'),
|
||||
allowExistingMembers: Ember.computed.alias('invitingToTopic'),
|
||||
|
||||
@computed("isAdmin", "model.group_users")
|
||||
isGroupOwnerOrAdmin(isAdmin, groupUsers) {
|
||||
return isAdmin || (groupUsers && groupUsers.some(groupUser => groupUser.owner));
|
||||
},
|
||||
|
||||
// Show Groups? (add invited user to private group)
|
||||
@computed('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic', 'canInviteViaEmail')
|
||||
showGroups(isAdmin, emailOrUsername, isPrivateTopic, isMessage, invitingToTopic, canInviteViaEmail) {
|
||||
return isAdmin &&
|
||||
@computed('isGroupOwnerOrAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic', 'canInviteViaEmail')
|
||||
showGroups(isGroupOwnerOrAdmin, emailOrUsername, isPrivateTopic, isMessage, invitingToTopic, canInviteViaEmail) {
|
||||
return isGroupOwnerOrAdmin &&
|
||||
canInviteViaEmail &&
|
||||
!isMessage &&
|
||||
(emailValid(emailOrUsername) || isPrivateTopic || !invitingToTopic);
|
||||
|
@ -139,30 +149,34 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
}
|
||||
},
|
||||
|
||||
showGroupsClass: function() {
|
||||
return this.get('isPrivateTopic') ? 'required' : 'optional';
|
||||
}.property('isPrivateTopic'),
|
||||
|
||||
groupFinder(term) {
|
||||
const Group = require('discourse/models/group').default;
|
||||
return Group.findAll({search: term, ignore_automatic: true});
|
||||
@computed('isPrivateTopic')
|
||||
showGroupsClass(isPrivateTopic) {
|
||||
return isPrivateTopic ? 'required' : 'optional';
|
||||
},
|
||||
|
||||
successMessage: function() {
|
||||
groupFinder(term) {
|
||||
return Group.findAll({ term: term, ignore_automatic: true });
|
||||
},
|
||||
|
||||
@computed('isMessage', 'emailOrUsername', 'invitingExistingUserToTopic')
|
||||
successMessage(isMessage, emailOrUsername, invitingExistingUserToTopic) {
|
||||
if (this.get('hasGroups')) {
|
||||
return I18n.t('topic.invite_private.success_group');
|
||||
} else if (this.get('isMessage')) {
|
||||
} else if (isMessage) {
|
||||
return I18n.t('topic.invite_private.success');
|
||||
} else if ( emailValid(this.get('emailOrUsername')) ) {
|
||||
return I18n.t('topic.invite_reply.success_email', { emailOrUsername: this.get('emailOrUsername') });
|
||||
} else if (invitingExistingUserToTopic) {
|
||||
return I18n.t('topic.invite_reply.success_existing_email', { emailOrUsername });
|
||||
} else if (emailValid(emailOrUsername)) {
|
||||
return I18n.t('topic.invite_reply.success_email', { emailOrUsername });
|
||||
} else {
|
||||
return I18n.t('topic.invite_reply.success_username');
|
||||
}
|
||||
}.property('model.inviteLink', 'isMessage', 'emailOrUsername'),
|
||||
},
|
||||
|
||||
errorMessage: function() {
|
||||
return this.get('isMessage') ? I18n.t('topic.invite_private.error') : I18n.t('topic.invite_reply.error');
|
||||
}.property('isMessage'),
|
||||
@computed('isMessage')
|
||||
errorMessage(isMessage) {
|
||||
return isMessage ? I18n.t('topic.invite_private.error') : I18n.t('topic.invite_reply.error');
|
||||
},
|
||||
|
||||
@computed('canInviteViaEmail')
|
||||
placeholderKey(canInviteViaEmail) {
|
||||
|
@ -171,15 +185,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
'topic.invite_reply.username_placeholder';
|
||||
},
|
||||
|
||||
customMessagePlaceholder: function() {
|
||||
@computed
|
||||
customMessagePlaceholder() {
|
||||
return I18n.t('invite.custom_message_placeholder');
|
||||
}.property(),
|
||||
},
|
||||
|
||||
// Reset the modal to allow a new user to be invited.
|
||||
reset() {
|
||||
this.set('emailOrUsername', null);
|
||||
this.set('hasCustomMessage', false);
|
||||
this.set('customMessage', null);
|
||||
this.set('invitingExistingUserToTopic', false);
|
||||
this.get('model').setProperties({
|
||||
groupNames: null,
|
||||
error: false,
|
||||
|
@ -188,12 +204,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
inviteLink: null
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
createInvite() {
|
||||
const Invite = require('discourse/models/invite').default;
|
||||
const self = this;
|
||||
|
||||
if (this.get('disabled')) { return; }
|
||||
|
||||
const groupNames = this.get('model.groupNames'),
|
||||
|
@ -231,13 +246,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
} else if (this.get('isMessage') && result && result.user) {
|
||||
this.get('model.details.allowed_users').pushObject(Ember.Object.create(result.user));
|
||||
this.appEvents.trigger('post-stream:refresh');
|
||||
} else if (this.get('invitingToTopic') && emailValid(this.get('emailOrUsername').trim()) && result && result.user) {
|
||||
this.set('invitingExistingUserToTopic', true);
|
||||
}
|
||||
}).catch(onerror);
|
||||
}
|
||||
},
|
||||
|
||||
generateInvitelink() {
|
||||
const Invite = require('discourse/models/invite').default;
|
||||
const self = this;
|
||||
|
||||
if (this.get('disabled')) { return; }
|
||||
|
|
|
@ -5,15 +5,17 @@ import { ajax } from 'discourse/lib/ajax';
|
|||
import PasswordValidation from "discourse/mixins/password-validation";
|
||||
import UsernameValidation from "discourse/mixins/username-validation";
|
||||
import NameValidation from "discourse/mixins/name-validation";
|
||||
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
|
||||
import { findAll as findLoginMethods } from 'discourse/models/login-method';
|
||||
|
||||
export default Ember.Controller.extend(PasswordValidation, UsernameValidation, NameValidation, {
|
||||
export default Ember.Controller.extend(PasswordValidation, UsernameValidation, NameValidation, UserFieldsValidation, {
|
||||
invitedBy: Ember.computed.alias('model.invited_by'),
|
||||
email: Ember.computed.alias('model.email'),
|
||||
accountUsername: Ember.computed.alias('model.username'),
|
||||
passwordRequired: Ember.computed.notEmpty('accountPassword'),
|
||||
successMessage: null,
|
||||
errorMessage: null,
|
||||
userFields: null,
|
||||
inviteImageUrl: getUrl('/images/envelope.svg'),
|
||||
|
||||
@computed
|
||||
|
@ -21,11 +23,6 @@ export default Ember.Controller.extend(PasswordValidation, UsernameValidation, N
|
|||
return I18n.t('invites.welcome_to', {site_name: this.siteSettings.title});
|
||||
},
|
||||
|
||||
@computed
|
||||
nameLabel() {
|
||||
return I18n.t(this.siteSettings.full_name_required ? 'invites.name_label' : 'invites.name_label_optional');
|
||||
},
|
||||
|
||||
@computed('email')
|
||||
yourEmailMessage(email) {
|
||||
return I18n.t('invites.your_email', {email: email});
|
||||
|
@ -36,20 +33,30 @@ export default Ember.Controller.extend(PasswordValidation, UsernameValidation, N
|
|||
return findLoginMethods(this.siteSettings, this.capabilities, this.site.isMobileDevice).length > 0;
|
||||
},
|
||||
|
||||
@computed('usernameValidation.failed', 'passwordValidation.failed', 'nameValidation.failed')
|
||||
submitDisabled(usernameFailed, passwordFailed, nameFailed) {
|
||||
return usernameFailed || passwordFailed || nameFailed;
|
||||
@computed('usernameValidation.failed', 'passwordValidation.failed', 'nameValidation.failed', 'userFieldsValidation.failed')
|
||||
submitDisabled(usernameFailed, passwordFailed, nameFailed, userFieldsFailed) {
|
||||
return usernameFailed || passwordFailed || nameFailed || userFieldsFailed;
|
||||
},
|
||||
|
||||
actions: {
|
||||
submit() {
|
||||
|
||||
const userFields = this.get('userFields');
|
||||
let userCustomFields = {};
|
||||
if (!Ember.isEmpty(userFields)) {
|
||||
userFields.forEach(function(f) {
|
||||
userCustomFields[f.get('field.id')] = f.get('value');
|
||||
});
|
||||
}
|
||||
|
||||
ajax({
|
||||
url: `/invites/show/${this.get('model.token')}.json`,
|
||||
type: 'PUT',
|
||||
data: {
|
||||
username: this.get('accountUsername'),
|
||||
name: this.get('accountName'),
|
||||
password: this.get('accountPassword')
|
||||
password: this.get('accountPassword'),
|
||||
userCustomFields
|
||||
}
|
||||
}).then(result => {
|
||||
if (result.success) {
|
||||
|
|
|
@ -47,8 +47,6 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
|||
const model = this.get('model'),
|
||||
userFields = this.get('userFields');
|
||||
|
||||
model.set('name', this.get('newNameInput'));
|
||||
|
||||
// Update the user fields
|
||||
if (!Ember.isEmpty(userFields)) {
|
||||
const modelFields = model.get('user_fields');
|
||||
|
|
|
@ -4,6 +4,9 @@ const _buttons = [];
|
|||
|
||||
const alwaysTrue = () => true;
|
||||
|
||||
function identity() {
|
||||
}
|
||||
|
||||
function addBulkButton(action, key, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
|
@ -72,7 +75,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
this.perform(operation).then(topics => {
|
||||
if (topics) {
|
||||
topics.forEach(cb);
|
||||
(this.get('refreshClosure') || Ember.k)();
|
||||
(this.get('refreshClosure') || identity)();
|
||||
this.send('closeModal');
|
||||
}
|
||||
});
|
||||
|
@ -80,7 +83,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
performAndRefresh(operation) {
|
||||
return this.perform(operation).then(() => {
|
||||
(this.get('refreshClosure') || Ember.k)();
|
||||
(this.get('refreshClosure') || identity)();
|
||||
this.send('closeModal');
|
||||
});
|
||||
},
|
||||
|
@ -145,7 +148,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
this.perform({type: 'change_category', category_id: categoryId}).then(topics => {
|
||||
topics.forEach(t => t.set('category', category));
|
||||
(this.get('refreshClosure') || Ember.k)();
|
||||
(this.get('refreshClosure') || identity)();
|
||||
this.send('closeModal');
|
||||
});
|
||||
},
|
||||
|
|
|
@ -196,7 +196,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
const quotedText = Quote.build(post, buffer);
|
||||
composerOpts.quote = quotedText;
|
||||
if (composer.get('model.viewOpen')) {
|
||||
this.appEvents.trigger('composer:insert-text', quotedText);
|
||||
this.appEvents.trigger('composer:insert-block', quotedText);
|
||||
} else if (composer.get('model.viewDraft')) {
|
||||
const model = composer.get('model');
|
||||
model.set('reply', model.get('reply') + quotedText);
|
||||
|
@ -320,7 +320,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
composerController.get('content.action') === Composer.REPLY) {
|
||||
composerController.set('content.post', post);
|
||||
composerController.set('content.composeState', Composer.OPEN);
|
||||
this.appEvents.trigger('composer:insert-text', quotedText.trim());
|
||||
this.appEvents.trigger('composer:insert-block', quotedText.trim());
|
||||
} else {
|
||||
|
||||
const opts = {
|
||||
|
|
|
@ -12,6 +12,7 @@ export default Ember.Controller.extend({
|
|||
canLoadMore: true,
|
||||
invitesLoading: false,
|
||||
reinvitedAll: false,
|
||||
rescindedAll: false,
|
||||
|
||||
init: function() {
|
||||
this._super();
|
||||
|
@ -32,7 +33,7 @@ export default Ember.Controller.extend({
|
|||
|
||||
inviteRedeemed: Em.computed.equal('filter', 'redeemed'),
|
||||
|
||||
showReinviteAllButton: function() {
|
||||
showBulkActionButtons: function() {
|
||||
return (this.get('filter') === "pending" && this.get('model').invites.length > 4 && this.currentUser.get('staff'));
|
||||
}.property('filter'),
|
||||
|
||||
|
@ -86,17 +87,27 @@ export default Ember.Controller.extend({
|
|||
return false;
|
||||
},
|
||||
|
||||
rescindAll() {
|
||||
bootbox.confirm(I18n.t("user.invited.rescind_all_confirm"), confirm => {
|
||||
if (confirm) {
|
||||
Invite.rescindAll().then(() => {
|
||||
this.set('rescindedAll', true);
|
||||
this.get('model.invites').clear();
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
reinvite(invite) {
|
||||
invite.reinvite();
|
||||
return false;
|
||||
},
|
||||
|
||||
reinviteAll() {
|
||||
const self = this;
|
||||
bootbox.confirm(I18n.t("user.invited.reinvite_all_confirm"), confirm => {
|
||||
if (confirm) {
|
||||
Invite.reinviteAll().then(function() {
|
||||
self.set('reinvitedAll', true);
|
||||
Invite.reinviteAll().then(() => {
|
||||
this.set('reinvitedAll', true);
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -87,7 +87,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
|
||||
adminDelete() {
|
||||
// I really want this deferred, don't want to bring in all this code till used
|
||||
const AdminUser = require('admin/models/admin-user').default;
|
||||
const AdminUser = requirejs('admin/models/admin-user').default;
|
||||
AdminUser.find(this.get('model.id')).then(user => user.destroy({deletePosts: true}));
|
||||
},
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
import { cook } from 'discourse/lib/text';
|
||||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||
|
||||
registerUnbound('cook-text', cook);
|
|
@ -20,7 +20,7 @@ function renderRaw(ctx, container, template, templateName, params) {
|
|||
|
||||
const module = `discourse/raw-views/${templateName}`;
|
||||
if (requirejs.entries[module]) {
|
||||
const viewClass = require(module, null, null, true);
|
||||
const viewClass = requirejs(module, null, null, true);
|
||||
if (viewClass && viewClass.default) {
|
||||
params.view = viewClass.default.create(params, _injections);
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ import { registerHelpers } from 'discourse-common/lib/helpers';
|
|||
export function autoLoadModules(container, registry) {
|
||||
Object.keys(requirejs.entries).forEach(entry => {
|
||||
if ((/\/helpers\//).test(entry)) {
|
||||
require(entry, null, null, true);
|
||||
requirejs(entry, null, null, true);
|
||||
}
|
||||
if ((/\/widgets\//).test(entry)) {
|
||||
require(entry, null, null, true);
|
||||
requirejs(entry, null, null, true);
|
||||
}
|
||||
});
|
||||
registerHelpers(registry);
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
export default {
|
||||
name: "inject-objects",
|
||||
initialize: Ember.K
|
||||
initialize() { }
|
||||
};
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
export default {
|
||||
name: "register-discourse-location",
|
||||
initialize: Ember.K
|
||||
initialize() { }
|
||||
};
|
||||
|
|
|
@ -25,5 +25,8 @@ var transitionEnd = (function() {
|
|||
})();
|
||||
|
||||
export default function (element, callback) {
|
||||
return $(element).on(transitionEnd, callback);
|
||||
return $(element).on(transitionEnd, event => {
|
||||
if (event.target !== event.currentTarget) return;
|
||||
return callback(event);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -358,10 +358,22 @@ export default function(options) {
|
|||
$(this).on('keyup.autocomplete', function(e) {
|
||||
if ([keys.esc, keys.enter].indexOf(e.which) !== -1) return true;
|
||||
|
||||
var cp = caretPosition(me[0]);
|
||||
let cp = caretPosition(me[0]);
|
||||
const key = me[0].value[cp-1];
|
||||
|
||||
if (options.key && completeStart === null && cp > 0) {
|
||||
var key = me[0].value[cp-1];
|
||||
if (options.key) {
|
||||
if (options.onKeyUp && key !== options.key) {
|
||||
let match = options.onKeyUp(me.val(), cp);
|
||||
if (match) {
|
||||
completeStart = cp - match[0].length;
|
||||
completeEnd = completeStart + match[0].length - 1;
|
||||
let term = match[0].substring(1, match[0].length);
|
||||
updateAutoComplete(dataSource(term, options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (completeStart === null && cp > 0) {
|
||||
if (key === options.key) {
|
||||
var prevChar = me.val().charAt(cp-2);
|
||||
if (checkTriggerRule() && (!prevChar || allowedLettersRegex.test(prevChar))) {
|
||||
|
@ -370,7 +382,7 @@ export default function(options) {
|
|||
}
|
||||
}
|
||||
} else if (completeStart !== null) {
|
||||
var term = me.val().substring(completeStart + (options.key ? 1 : 0), cp);
|
||||
let term = me.val().substring(completeStart + (options.key ? 1 : 0), cp);
|
||||
updateAutoComplete(dataSource(term, options));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -132,15 +132,9 @@ function assign(ta, {setOverflowX = true, setOverflowY = true} = {}) {
|
|||
set.delete(ta);
|
||||
|
||||
Object.keys(style).forEach(key => {
|
||||
ta.style[key] = style[key];
|
||||
ta.style[key] = style[key];
|
||||
});
|
||||
}.bind(ta, {
|
||||
height: ta.style.height,
|
||||
resize: ta.style.resize,
|
||||
overflowY: ta.style.overflowY,
|
||||
overflowX: ta.style.overflowX,
|
||||
wordWrap: ta.style.wordWrap,
|
||||
});
|
||||
}
|
||||
|
||||
ta.addEventListener('autosize:destroy', destroy, false);
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
export default class DirtyKeys {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this._keys = {};
|
||||
}
|
||||
|
||||
keyDirty(key, options) {
|
||||
options = options || {};
|
||||
options.dirty = true;
|
||||
this._keys[key] = options;
|
||||
}
|
||||
|
||||
forceAll() {
|
||||
this.keyDirty('*');
|
||||
}
|
||||
|
||||
allDirty() {
|
||||
return !!this._keys['*'];
|
||||
}
|
||||
|
||||
optionsFor(key) {
|
||||
return this._keys[key] || { dirty: false };
|
||||
}
|
||||
|
||||
renderedKey(key) {
|
||||
if (key === '*') {
|
||||
this._keys = {};
|
||||
} else {
|
||||
delete this._keys[key];
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,183 +0,0 @@
|
|||
import groups from 'discourse/lib/emoji/groups';
|
||||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import { emojiList } from 'pretty-text/emoji';
|
||||
import { emojiUrlFor } from 'discourse/lib/text';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
const keyValueStore = new KeyValueStore("discourse_emojis_");
|
||||
const EMOJI_USAGE = "emojiUsage";
|
||||
|
||||
let PER_ROW = 12;
|
||||
const PER_PAGE = 60;
|
||||
|
||||
let ungroupedIcons, recentlyUsedIcons;
|
||||
|
||||
if (!keyValueStore.getObject(EMOJI_USAGE)) {
|
||||
keyValueStore.setObject({key: EMOJI_USAGE, value: {}});
|
||||
}
|
||||
|
||||
function closeSelector() {
|
||||
$('.emoji-modal, .emoji-modal-wrapper').remove();
|
||||
$('body, textarea').off('keydown.emoji');
|
||||
}
|
||||
|
||||
function initializeUngroupedIcons() {
|
||||
const groupedIcons = {};
|
||||
|
||||
groups.forEach(group => {
|
||||
group.icons.forEach(icon => groupedIcons[icon] = true);
|
||||
});
|
||||
|
||||
ungroupedIcons = [];
|
||||
const emojis = emojiList();
|
||||
emojis.forEach(emoji => {
|
||||
if (groupedIcons[emoji] !== true) {
|
||||
ungroupedIcons.push(emoji);
|
||||
}
|
||||
});
|
||||
|
||||
if (ungroupedIcons.length) {
|
||||
groups.push({name: 'ungrouped', icons: ungroupedIcons});
|
||||
}
|
||||
}
|
||||
|
||||
function trackEmojiUsage(title) {
|
||||
const recent = keyValueStore.getObject(EMOJI_USAGE) || {};
|
||||
|
||||
if (!recent[title]) { recent[title] = { title: title, usage: 0 }; }
|
||||
recent[title]["usage"]++;
|
||||
|
||||
keyValueStore.setObject({key: EMOJI_USAGE, value: recent});
|
||||
|
||||
// clear the cache
|
||||
recentlyUsedIcons = null;
|
||||
}
|
||||
|
||||
function sortByUsage(a, b) {
|
||||
if (a.usage > b.usage) { return -1; }
|
||||
if (b.usage > a.usage) { return 1; }
|
||||
return a.title.localeCompare(b.title);
|
||||
}
|
||||
|
||||
function initializeRecentlyUsedIcons() {
|
||||
recentlyUsedIcons = [];
|
||||
|
||||
const usage = _.map(keyValueStore.getObject(EMOJI_USAGE)).sort(sortByUsage);
|
||||
const recent = usage.slice(0, PER_ROW);
|
||||
|
||||
if (recent.length > 0) {
|
||||
|
||||
recent.forEach(emoji => recentlyUsedIcons.push(emoji.title));
|
||||
|
||||
const recentGroup = groups.findBy('name', 'recent');
|
||||
if (recentGroup) {
|
||||
recentGroup.icons = recentlyUsedIcons;
|
||||
} else {
|
||||
groups.push({ name: 'recent', icons: recentlyUsedIcons });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toolbar(selected) {
|
||||
if (!ungroupedIcons) { initializeUngroupedIcons(); }
|
||||
if (!recentlyUsedIcons) { initializeRecentlyUsedIcons(); }
|
||||
|
||||
return groups.map((g, i) => {
|
||||
let icon = g.tabicon;
|
||||
let title = g.fullname;
|
||||
if (g.name === "recent") {
|
||||
icon = "star";
|
||||
title = "Recent";
|
||||
} else if (g.name === "ungrouped") {
|
||||
icon = g.icons[0];
|
||||
title = "Custom";
|
||||
}
|
||||
|
||||
return { src: emojiUrlFor(icon),
|
||||
title,
|
||||
groupId: i,
|
||||
selected: i === selected };
|
||||
});
|
||||
}
|
||||
|
||||
function bindEvents(page, offset, options) {
|
||||
$('.emoji-page a').click(e => {
|
||||
const title = $(e.currentTarget).attr('title');
|
||||
trackEmojiUsage(title);
|
||||
options.onSelect(title);
|
||||
closeSelector();
|
||||
return false;
|
||||
}).hover(e => {
|
||||
const title = $(e.currentTarget).attr('title');
|
||||
const html = "<img src='" + emojiUrlFor(title) + "' class='emoji'> <span>:" + title + ":<span>";
|
||||
$('.emoji-modal .info').html(html);
|
||||
}, () => $('.emoji-modal .info').html(""));
|
||||
|
||||
$('.emoji-modal .nav .next a').click(() => render(page, offset+PER_PAGE, options));
|
||||
$('.emoji-modal .nav .prev a').click(() => render(page, offset-PER_PAGE, options));
|
||||
|
||||
$('.emoji-modal .toolbar a').click(function(){
|
||||
const p = parseInt($(this).data('group-id'));
|
||||
render(p, 0, options);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function render(page, offset, options) {
|
||||
keyValueStore.set({key: "emojiPage", value: page});
|
||||
keyValueStore.set({key: "emojiOffset", value: offset});
|
||||
|
||||
const toolbarItems = toolbar(page);
|
||||
const rows = [];
|
||||
let row = [];
|
||||
const icons = groups[page].icons;
|
||||
const max = offset + PER_PAGE;
|
||||
|
||||
for(let i=offset; i<max; i++){
|
||||
if(!icons[i]){ break; }
|
||||
if(row.length === (options.perRow || PER_ROW)){
|
||||
rows.push(row);
|
||||
row = [];
|
||||
}
|
||||
row.push({src: emojiUrlFor(icons[i]), title: icons[i]});
|
||||
}
|
||||
rows.push(row);
|
||||
|
||||
const model = {
|
||||
toolbarItems: toolbarItems,
|
||||
rows: rows,
|
||||
prevDisabled: offset === 0,
|
||||
nextDisabled: (max + 1) > icons.length,
|
||||
modalClass: options.modalClass
|
||||
};
|
||||
|
||||
$('.emoji-modal', options.appendTo).remove();
|
||||
const template = findRawTemplate('emoji-toolbar');
|
||||
options.appendTo.append(template(model));
|
||||
|
||||
bindEvents(page, offset, options);
|
||||
}
|
||||
|
||||
function showSelector(options) {
|
||||
options = options || {};
|
||||
options.appendTo = options.appendTo || $('body');
|
||||
|
||||
options.appendTo.append('<div class="emoji-modal-wrapper"></div>');
|
||||
$('.emoji-modal-wrapper').click(() => closeSelector());
|
||||
|
||||
if (Discourse.Site.currentProp('mobileView')) { PER_ROW = 9; }
|
||||
const page = options.page ? _.findIndex(groups, (g) => { return g.name === options.page; })
|
||||
: keyValueStore.getInt("emojiPage", 0);
|
||||
const offset = keyValueStore.getInt("emojiOffset", 0);
|
||||
|
||||
render(page, offset, options);
|
||||
|
||||
$('body, textarea').on('keydown.emoji', e => {
|
||||
if (e.which === 27) {
|
||||
closeSelector();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { showSelector };
|
|
@ -0,0 +1,8 @@
|
|||
export default function($elem, term) {
|
||||
if(!_.isEmpty(term)) {
|
||||
// special case ignore "l" which is used for magic sorting
|
||||
let words = _.reject(term.match(/"[^"]+"|[^\s]+/g), t => t === 'l');
|
||||
words = words.map(w => w.replace(/^"(.*)"$/, "$1"));
|
||||
$elem.highlight(words, {className: 'search-highlight', wordsOnly: true});
|
||||
}
|
||||
}
|
|
@ -36,7 +36,11 @@ export default function loadScript(url, opts) {
|
|||
opts = opts || {};
|
||||
|
||||
$('script').each((i, tag) => {
|
||||
_loaded[tag.getAttribute('src')] = true;
|
||||
const src = tag.getAttribute('src');
|
||||
|
||||
if (src && (opts.scriptTag || src !== url)) {
|
||||
_loaded[tag.getAttribute('src')] = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
@ -57,12 +61,12 @@ export default function loadScript(url, opts) {
|
|||
});
|
||||
|
||||
const cb = function(data) {
|
||||
_loaded[url] = true;
|
||||
if (opts && opts.css) {
|
||||
$("head").append("<style>" + data + "</style>");
|
||||
}
|
||||
done();
|
||||
resolve();
|
||||
_loaded[url] = true;
|
||||
};
|
||||
|
||||
let cdnUrl = url;
|
||||
|
|
|
@ -22,7 +22,7 @@ import { attachAdditionalPanel } from 'discourse/widgets/header';
|
|||
|
||||
|
||||
// If you add any methods to the API ensure you bump up this number
|
||||
const PLUGIN_API_VERSION = '0.8.6';
|
||||
const PLUGIN_API_VERSION = '0.8.7';
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
|
@ -39,6 +39,25 @@ class PluginApi {
|
|||
return this.container.lookup('current-user:main');
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to overwrite or extend methods in a class.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* ```
|
||||
* api.modifyClass('controller:composer', {
|
||||
* actions: {
|
||||
* newActionHere() { }
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
**/
|
||||
modifyClass(resolverName, changes) {
|
||||
const klass = this.container.factoryFor(resolverName);
|
||||
klass.class.reopen(changes);
|
||||
return klass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for decorating the `cooked` content of a post after it is rendered using
|
||||
* jQuery.
|
||||
|
@ -61,7 +80,7 @@ class PluginApi {
|
|||
|
||||
if (!opts.onlyStream) {
|
||||
decorate(ComposerEditor, 'previewRefreshed', callback);
|
||||
decorate(this.container.lookupFactory('component:user-stream'), 'didInsertElement', callback);
|
||||
decorate(this.container.factoryFor('component:user-stream').class, 'didInsertElement', callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,7 +189,7 @@ class PluginApi {
|
|||
* ```
|
||||
**/
|
||||
attachWidgetAction(widget, actionName, fn) {
|
||||
const widgetClass = this.container.lookupFactory(`widget:${widget}`);
|
||||
const widgetClass = this.container.factoryFor(`widget:${widget}`).class;
|
||||
widgetClass.prototype[actionName] = fn;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ function findClass(outletName, uniqueName) {
|
|||
if (!_classPaths) {
|
||||
_classPaths = {};
|
||||
findOutlets(require._eak_seen, (outlet, res, un) => {
|
||||
_classPaths[`${outlet}/${un}`] = require(res).default;
|
||||
_classPaths[`${outlet}/${un}`] = requirejs(res).default;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ export default Ember.Object.extend(Ember.Array, {
|
|||
},
|
||||
|
||||
finishedPrepending(postIds) {
|
||||
this._changeArray(Ember.K, 0, 0, postIds.length);
|
||||
this._changeArray(function() { }, 0, 0, postIds.length);
|
||||
},
|
||||
|
||||
objectAt(index) {
|
||||
|
|
|
@ -7,8 +7,8 @@ export function isAppleDevice() {
|
|||
}
|
||||
|
||||
|
||||
// we can't tell what the actual visible window height is
|
||||
// because we cannot account for the height of the mobile keyboard
|
||||
// we can't tell what the actual visible window height is
|
||||
// because we cannot account for the height of the mobile keyboard
|
||||
// and any other mobile autocomplete UI that may appear
|
||||
// so let's be conservative here rather than trying to max out every
|
||||
// available pixel of height for the editor
|
||||
|
@ -123,6 +123,10 @@ function positioningWorkaround($fixedElement) {
|
|||
|
||||
const checkForInputs = _.debounce(function(){
|
||||
$fixedElement.find('button:not(.hide-preview),a:not(.mobile-file-upload):not(.toggle-toolbar)').each(function(idx, elem){
|
||||
if ($(elem).parents('.emoji-picker').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($(elem).parents('.autocomplete').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -6,13 +6,11 @@ import Category from 'discourse/models/category';
|
|||
import { search as searchCategoryTag } from 'discourse/lib/category-tag-search';
|
||||
import userSearch from 'discourse/lib/user-search';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
import User from 'discourse/models/user';
|
||||
import Post from 'discourse/models/post';
|
||||
import Topic from 'discourse/models/topic';
|
||||
|
||||
export function translateResults(results, opts) {
|
||||
|
||||
const User = require('discourse/models/user').default;
|
||||
const Post = require('discourse/models/post').default;
|
||||
const Topic = require('discourse/models/topic').default;
|
||||
|
||||
if (!opts) opts = {};
|
||||
|
||||
// Topics might not be included
|
||||
|
@ -94,9 +92,9 @@ export function searchForTerm(term, opts) {
|
|||
};
|
||||
}
|
||||
|
||||
var promise = ajax('/search/query', { data: data });
|
||||
let promise = ajax('/search/query', { data: data });
|
||||
|
||||
promise.then(function(results){
|
||||
promise.then(results => {
|
||||
return translateResults(results, opts);
|
||||
});
|
||||
|
||||
|
|
|
@ -2,24 +2,40 @@ import { default as PrettyText, buildOptions } from 'pretty-text/pretty-text';
|
|||
import { performEmojiUnescape, buildEmojiUrl } from 'pretty-text/emoji';
|
||||
import WhiteLister from 'pretty-text/white-lister';
|
||||
import { sanitize as textSanitize } from 'pretty-text/sanitizer';
|
||||
import loadScript from 'discourse/lib/load-script';
|
||||
|
||||
function getOpts() {
|
||||
function getOpts(opts) {
|
||||
const siteSettings = Discourse.__container__.lookup('site-settings:main');
|
||||
|
||||
return buildOptions({
|
||||
opts = _.merge({
|
||||
getURL: Discourse.getURLWithCDN,
|
||||
currentUser: Discourse.__container__.lookup('current-user:main'),
|
||||
siteSettings
|
||||
});
|
||||
}, opts);
|
||||
|
||||
return buildOptions(opts);
|
||||
}
|
||||
|
||||
// Use this to easily create a pretty text instance with proper options
|
||||
export function cook(text) {
|
||||
return new Handlebars.SafeString(new PrettyText(getOpts()).cook(text));
|
||||
export function cook(text, options) {
|
||||
return new Handlebars.SafeString(new PrettyText(getOpts(options)).cook(text));
|
||||
}
|
||||
|
||||
export function sanitize(text) {
|
||||
return textSanitize(text, new WhiteLister(getOpts()));
|
||||
// everything should eventually move to async API and this should be renamed
|
||||
// cook
|
||||
export function cookAsync(text, options) {
|
||||
if (Discourse.MarkdownItURL) {
|
||||
return loadScript(Discourse.MarkdownItURL)
|
||||
.then(()=>cook(text, options))
|
||||
.catch(e => Ember.Logger.error(e));
|
||||
} else {
|
||||
return Ember.RSVP.Promise.resolve(cook(text));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function sanitize(text, options) {
|
||||
return textSanitize(text, new WhiteLister(options));
|
||||
}
|
||||
|
||||
function emojiOptions() {
|
||||
|
|
|
@ -14,6 +14,7 @@ const SERVER_SIDE_ONLY = [
|
|||
/^\/raw\//,
|
||||
/^\/posts\/\d+\/raw/,
|
||||
/^\/raw\/\d+/,
|
||||
/^\/wizard/,
|
||||
/\.rss$/,
|
||||
/\.json$/,
|
||||
];
|
||||
|
@ -221,6 +222,11 @@ const DiscourseURL = Ember.Object.extend({
|
|||
// TODO: Extract into rules we can inject into the URL handler
|
||||
if (this.navigatedToHome(oldPath, path, opts)) { return; }
|
||||
|
||||
// Navigating to empty string is the same as root
|
||||
if (path === '') {
|
||||
path = '/';
|
||||
}
|
||||
|
||||
return this.handleURL(path, opts);
|
||||
},
|
||||
|
||||
|
@ -367,7 +373,7 @@ const DiscourseURL = Ember.Object.extend({
|
|||
discoveryTopics.resetParams();
|
||||
}
|
||||
|
||||
router.router.updateURL(path);
|
||||
router._routerMicrolib.updateURL(path);
|
||||
}
|
||||
|
||||
const split = path.split('#');
|
||||
|
|
|
@ -102,8 +102,10 @@ export function selectedText() {
|
|||
$div.find("img.emoji").replaceWith(function() { return this.title; });
|
||||
// replace br with newlines
|
||||
$div.find("br").replaceWith(() => "\n");
|
||||
// enforce newline at the end of paragraphs
|
||||
$div.find("p").append(() => "\n");
|
||||
|
||||
return String($div.text()).trim();
|
||||
return String($div.text()).trim().replace(/(^\s*\n)+/gm, "\n");
|
||||
}
|
||||
|
||||
// Determine the row and col of the caret in an element
|
||||
|
@ -172,7 +174,7 @@ export function validateUploadedFiles(files, opts) {
|
|||
}
|
||||
|
||||
opts = opts || {};
|
||||
opts["type"] = uploadTypeFromFileName(upload.name);
|
||||
opts.type = uploadTypeFromFileName(upload.name);
|
||||
|
||||
return validateUploadedFile(upload, opts);
|
||||
}
|
||||
|
@ -185,12 +187,18 @@ export function validateUploadedFile(file, opts) {
|
|||
if (!name) { return false; }
|
||||
|
||||
// check that the uploaded file is authorized
|
||||
if (opts["imagesOnly"]) {
|
||||
if (opts.allowStaffToUploadAnyFileInPm && opts.isPrivateMessage) {
|
||||
if (Discourse.User.current("staff")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.imagesOnly) {
|
||||
if (!isAnImage(name) && !isAuthorizedImage(name)) {
|
||||
bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: authorizedImagesExtensions() }));
|
||||
return false;
|
||||
}
|
||||
} else if (opts["csvOnly"]) {
|
||||
} else if (opts.csvOnly) {
|
||||
if (!(/\.csv$/i).test(name)) {
|
||||
bootbox.alert(I18n.t('user.invited.bulk_invite.error'));
|
||||
return false;
|
||||
|
@ -202,10 +210,10 @@ export function validateUploadedFile(file, opts) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!opts["bypassNewUserRestriction"]) {
|
||||
if (!opts.bypassNewUserRestriction) {
|
||||
// ensures that new users can upload a file
|
||||
if (!Discourse.User.current().isAllowedToUploadAFile(opts["type"])) {
|
||||
bootbox.alert(I18n.t(`post.errors.${opts["type"]}_upload_not_allowed_for_new_user`));
|
||||
if (!Discourse.User.current().isAllowedToUploadAFile(opts.type)) {
|
||||
bootbox.alert(I18n.t(`post.errors.${opts.type}_upload_not_allowed_for_new_user`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ export function mapRoutes() {
|
|||
// can define admin routes.
|
||||
Object.keys(requirejs._eak_seen).forEach(function(key) {
|
||||
if (/route-map$/.test(key)) {
|
||||
var module = require(key, null, null, true);
|
||||
var module = requirejs(key, null, null, true);
|
||||
if (!module || !module.default) { throw new Error(key + ' must export a route map.'); }
|
||||
|
||||
const mapObj = module.default;
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
// A mixin where hitting ESC calls `cancelled` and ctrl+enter calls `save.
|
||||
export default {
|
||||
keyDown(e) {
|
||||
if (e.which === 27) {
|
||||
this.sendAction('cancelled');
|
||||
return false;
|
||||
} else if (e.which === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
// CTRL+ENTER or CMD+ENTER
|
||||
this.sendAction('save');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -3,6 +3,11 @@ import { default as computed } from 'ember-addons/ember-computed-decorators';
|
|||
|
||||
export default Ember.Mixin.create({
|
||||
|
||||
@computed()
|
||||
nameInstructions() {
|
||||
return I18n.t(this.siteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
|
||||
},
|
||||
|
||||
// Validate the name.
|
||||
@computed('accountName')
|
||||
nameValidation() {
|
||||
|
|
|
@ -31,7 +31,7 @@ const Scrolling = Ember.Mixin.create({
|
|||
opts = opts || { debounce: 100 };
|
||||
|
||||
// So we can not call the scrolled event while transitioning
|
||||
const router = Discourse.__container__.lookup('router:main').router;
|
||||
const router = Discourse.__container__.lookup('router:main')._routerMicrolib;
|
||||
|
||||
let onScrollMethod = () => {
|
||||
if (router.activeTransition) { return; }
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import InputValidation from 'discourse/models/input-validation';
|
||||
import { on, default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
|
||||
@on('init')
|
||||
_createUserFields() {
|
||||
if (!this.site) { return; }
|
||||
|
||||
let userFields = this.site.get('user_fields');
|
||||
if (userFields) {
|
||||
userFields = _.sortBy(userFields, 'position').map(function(f) {
|
||||
return Ember.Object.create({ value: null, field: f });
|
||||
});
|
||||
}
|
||||
this.set('userFields', userFields);
|
||||
},
|
||||
|
||||
// Validate required fields
|
||||
@computed('userFields.@each.value')
|
||||
userFieldsValidation() {
|
||||
let userFields = this.get('userFields');
|
||||
if (userFields) { userFields = userFields.filterBy('field.required'); }
|
||||
if (!Ember.isEmpty(userFields)) {
|
||||
const anyEmpty = userFields.any(uf => {
|
||||
const val = uf.get('value');
|
||||
return !val || Ember.isEmpty(val);
|
||||
});
|
||||
if (anyEmpty) {
|
||||
return InputValidation.create({ failed: true });
|
||||
}
|
||||
}
|
||||
return InputValidation.create({ ok: true });
|
||||
}
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
import Post from 'discourse/models/post';
|
||||
|
||||
export default Post.extend({
|
||||
|
||||
_attachCategory: function () {
|
||||
const categoryId = this.get("category_id");
|
||||
if (categoryId) {
|
||||
this.set("category", Discourse.Category.findById(categoryId));
|
||||
}
|
||||
}.on("init"),
|
||||
|
||||
presentName: Ember.computed.or('name', 'username'),
|
||||
|
||||
sameUser: function() {
|
||||
return this.get("username") === Discourse.User.currentProp("username");
|
||||
}.property("username"),
|
||||
|
||||
descriptionKey: function () {
|
||||
if (this.get("reply_to_post_number")) {
|
||||
return this.get("sameUser") ? "you_replied_to_post" : "user_replied_to_post";
|
||||
} else {
|
||||
return this.get("sameUser") ? "you_replied_to_topic" : "user_replied_to_topic";
|
||||
}
|
||||
}.property("reply_to_post_number", "sameUser")
|
||||
|
||||
});
|
|
@ -2,7 +2,6 @@ import { ajax } from 'discourse/lib/ajax';
|
|||
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
|
||||
import GroupHistory from 'discourse/models/group-history';
|
||||
import RestModel from 'discourse/models/rest';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
const Group = RestModel.extend({
|
||||
limit: 50,
|
||||
|
@ -114,23 +113,27 @@ const Group = RestModel.extend({
|
|||
return aliasLevel === '99';
|
||||
},
|
||||
|
||||
@observes("visible", "canEveryoneMention")
|
||||
@observes("visibility_level", "canEveryoneMention")
|
||||
_updateAllowMembershipRequests() {
|
||||
if (!this.get('visible') || !this.get('canEveryoneMention')) {
|
||||
if (this.get('visibility_level') !== 0 || !this.get('canEveryoneMention')) {
|
||||
this.set ('allow_membership_requests', false);
|
||||
}
|
||||
},
|
||||
|
||||
@observes("visible")
|
||||
@observes("visibility_level")
|
||||
_updatePublic() {
|
||||
if (!this.get('visible')) this.set('public', false);
|
||||
let visibility_level = parseInt(this.get('visibility_level'));
|
||||
if (visibility_level !== 0) {
|
||||
this.set('public', false);
|
||||
this.set('allow_membership_requests', false);
|
||||
}
|
||||
},
|
||||
|
||||
asJSON() {
|
||||
return {
|
||||
name: this.get('name'),
|
||||
alias_level: this.get('alias_level'),
|
||||
visible: !!this.get('visible'),
|
||||
visibility_level: this.get('visibility_level'),
|
||||
automatic_membership_email_domains: this.get('emailDomains'),
|
||||
automatic_membership_retroactive: !!this.get('automatic_membership_retroactive'),
|
||||
title: this.get('title'),
|
||||
|
@ -202,12 +205,18 @@ const Group = RestModel.extend({
|
|||
data: { notification_level, user_id: userId },
|
||||
type: "POST"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
requestMembership() {
|
||||
return ajax(`/groups/${this.get('name')}/request_membership`, {
|
||||
type: "POST"
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Group.reopenClass({
|
||||
findAll(opts) {
|
||||
return ajax("/admin/groups.json", { data: opts }).then(function (groups){
|
||||
return ajax("/groups/search.json", { data: opts }).then(groups => {
|
||||
return groups.map(g => Group.create(g));
|
||||
});
|
||||
},
|
||||
|
@ -216,10 +225,6 @@ Group.reopenClass({
|
|||
return ajax("/groups/" + name + ".json").then(result => Group.create(result.basic_group));
|
||||
},
|
||||
|
||||
loadOwners(name) {
|
||||
return ajax('/groups/' + name + '/owners.json').catch(popupAjaxError);
|
||||
},
|
||||
|
||||
loadMembers(name, offset, limit, params) {
|
||||
return ajax('/groups/' + name + '/members.json', {
|
||||
data: _.extend({
|
||||
|
|
|
@ -58,6 +58,10 @@ Invite.reopenClass({
|
|||
|
||||
reinviteAll() {
|
||||
return ajax('/invites/reinvite-all', { type: 'POST' });
|
||||
},
|
||||
|
||||
rescindAll() {
|
||||
return ajax('/invites/rescind-all', { type: 'POST' });
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@ import computed from 'ember-addons/ember-computed-decorators';
|
|||
import { postUrl } from 'discourse/lib/utilities';
|
||||
import { cook } from 'discourse/lib/text';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
import Composer from 'discourse/models/composer';
|
||||
|
||||
const Post = RestModel.extend({
|
||||
|
||||
|
@ -104,7 +105,6 @@ const Post = RestModel.extend({
|
|||
|
||||
createProperties() {
|
||||
// composer only used once, defer the dependency
|
||||
const Composer = require('discourse/models/composer').default;
|
||||
const data = this.getProperties(Composer.serializedFieldsForCreate());
|
||||
data.reply_to_post_number = this.get('reply_to_post_number');
|
||||
data.image_sizes = this.get('imageSizes');
|
||||
|
|
|
@ -3,7 +3,7 @@ const RestModel = Ember.Object.extend({
|
|||
isCreated: Ember.computed.equal('__state', 'created'),
|
||||
isSaving: false,
|
||||
|
||||
afterUpdate: Ember.K,
|
||||
afterUpdate() { },
|
||||
|
||||
update(props) {
|
||||
if (this.get('isSaving')) { return Ember.RSVP.reject(); }
|
||||
|
|
|
@ -297,7 +297,16 @@ export default Ember.Object.extend({
|
|||
|
||||
if (existing) {
|
||||
delete obj.id;
|
||||
const klass = this.register.lookupFactory('model:' + type) || RestModel;
|
||||
let klass = this.register.lookupFactory('model:' + type);
|
||||
|
||||
if (klass && klass.class) {
|
||||
klass = klass.class;
|
||||
}
|
||||
|
||||
if (!klass) {
|
||||
klass = RestModel;
|
||||
}
|
||||
|
||||
existing.setProperties(klass.munge(obj));
|
||||
obj.id = id;
|
||||
return existing;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import AdminPost from 'discourse/models/admin-post';
|
||||
import UserAction from 'discourse/models/user-action';
|
||||
|
||||
export default Discourse.Model.extend({
|
||||
loaded: false,
|
||||
|
@ -36,7 +36,7 @@ export default Discourse.Model.extend({
|
|||
|
||||
return ajax(this.get("url"), { cache: false }).then(function (result) {
|
||||
if (result) {
|
||||
const posts = result.map(function (post) { return AdminPost.create(post); });
|
||||
const posts = result.map(function (post) { return UserAction.create(post); });
|
||||
self.get("content").pushObjects(posts);
|
||||
self.setProperties({
|
||||
loaded: true,
|
||||
|
|
|
@ -8,6 +8,7 @@ import mobile from 'discourse/lib/mobile';
|
|||
import { findAll } from 'discourse/models/login-method';
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
import Composer from 'discourse/models/composer';
|
||||
|
||||
function unlessReadOnly(method, message) {
|
||||
return function() {
|
||||
|
@ -58,7 +59,6 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
|||
reply = post ? window.location.protocol + "//" + window.location.host + post.get("url") : null;
|
||||
|
||||
// used only once, one less dependency
|
||||
const Composer = require('discourse/models/composer').default;
|
||||
return this.controllerFor('composer').open({
|
||||
action: Composer.PRIVATE_MESSAGE,
|
||||
usernames: recipient,
|
||||
|
@ -94,6 +94,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
|||
showCreateAccount: unlessReadOnly('handleShowCreateAccount', I18n.t("read_only_mode.login_disabled")),
|
||||
|
||||
showForgotPassword() {
|
||||
this.controllerFor('forgot-password').setProperties({ offerHelp: null, helpSeen: false });
|
||||
showModal('forgotPassword', { title: 'forgot_password.title' });
|
||||
},
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue