Merge branch 'master' into search_posts_by_filetype

This commit is contained in:
Neil Lalonde 2017-07-25 14:41:20 -04:00 committed by GitHub
commit d8c27e3871
18042 changed files with 120300 additions and 33700 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
export default Ember.Component.extend({
actions: {
// TODO: When on Ember 1.13, use a closure action
loadMore() {
this.sendAction('loadMore');
}
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import KeyEnterEscape from 'discourse/mixins/key-enter-escape';
export default Ember.Component.extend(KeyEnterEscape, {
elementId: 'topic-title',
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
import { cook } from 'discourse/lib/text';
import { registerUnbound } from 'discourse-common/lib/helpers';
registerUnbound('cook-text', cook);

View File

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

View File

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

View File

@ -2,5 +2,5 @@
export default {
name: "inject-objects",
initialize: Ember.K
initialize() { }
};

View File

@ -2,5 +2,5 @@
export default {
name: "register-discourse-location",
initialize: Ember.K
initialize() { }
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,6 +58,10 @@ Invite.reopenClass({
reinviteAll() {
return ajax('/invites/reinvite-all', { type: 'POST' });
},
rescindAll() {
return ajax('/invites/rescind-all', { type: 'POST' });
}
});

View File

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

View File

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

View File

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

View File

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

View File

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