Version bump
This commit is contained in:
commit
3989a0d9f9
15
.eslintrc
15
.eslintrc
@ -6,28 +6,27 @@
|
||||
"browser": true,
|
||||
"builtin": true
|
||||
},
|
||||
"ecmaVersion": 7,
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 7,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"globals":
|
||||
{"Ember":true,
|
||||
"jQuery":true,
|
||||
"$":true,
|
||||
"QUnit":true,
|
||||
"RSVP":true,
|
||||
"Discourse":true,
|
||||
"Em":true,
|
||||
"Handlebars":true,
|
||||
"I18n":true,
|
||||
"bootbox":true,
|
||||
"module":true,
|
||||
"moduleFor":true,
|
||||
"moduleForComponent":true,
|
||||
"Pretender":true,
|
||||
"sandbox":true,
|
||||
"controllerFor":true,
|
||||
"test":true,
|
||||
"ok":true,
|
||||
"not":true,
|
||||
"expect":true,
|
||||
"equal":true,
|
||||
"visit":true,
|
||||
"andThen":true,
|
||||
"click":true,
|
||||
@ -48,12 +47,8 @@
|
||||
"find":true,
|
||||
"sinon":true,
|
||||
"moment":true,
|
||||
"start":true,
|
||||
"_":true,
|
||||
"alert":true,
|
||||
"containsInstance":true,
|
||||
"deepEqual":true,
|
||||
"notEqual":true,
|
||||
"define":true,
|
||||
"require":true,
|
||||
"requirejs":true,
|
||||
|
||||
@ -47,7 +47,7 @@ before_install:
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-cakeday.git plugins/discourse-cakeday
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-slack-official.git plugins/discourse-slack-official
|
||||
- yarn global add eslint@3 babel-eslint
|
||||
- yarn global add eslint babel-eslint
|
||||
- eslint app/assets/javascripts
|
||||
- eslint --ext .es6 app/assets/javascripts
|
||||
- eslint --ext .es6 test/javascripts
|
||||
|
||||
9
Gemfile
9
Gemfile
@ -36,6 +36,7 @@ end
|
||||
|
||||
gem 'mail'
|
||||
gem 'mime-types', require: 'mime/types/columnar'
|
||||
gem 'mini_mime'
|
||||
|
||||
gem 'hiredis'
|
||||
gem 'redis', require: ["redis", "redis/connection/hiredis"]
|
||||
@ -74,6 +75,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 +99,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
|
||||
|
||||
35
Gemfile.lock
35
Gemfile.lock
@ -42,7 +42,9 @@ 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)
|
||||
@ -86,8 +88,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 +101,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 +130,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)
|
||||
@ -160,6 +158,7 @@ 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)
|
||||
@ -173,7 +172,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 +183,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 +215,7 @@ GEM
|
||||
omniauth-twitter (1.3.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.12)
|
||||
onebox (1.8.13)
|
||||
fast_blank (>= 1.0.0)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
@ -288,10 +289,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 +316,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 +343,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)
|
||||
@ -432,6 +428,7 @@ DEPENDENCIES
|
||||
memory_profiler
|
||||
message_bus
|
||||
mime-types
|
||||
mini_mime
|
||||
mini_racer
|
||||
minitest
|
||||
mocha
|
||||
@ -439,6 +436,7 @@ DEPENDENCIES
|
||||
multi_json
|
||||
mustache
|
||||
nokogiri
|
||||
oga
|
||||
oj
|
||||
omniauth
|
||||
omniauth-facebook
|
||||
@ -465,7 +463,6 @@ DEPENDENCIES
|
||||
rbtrace
|
||||
redis
|
||||
redis-namespace
|
||||
rest-client
|
||||
rinku
|
||||
rmmseg-cpp
|
||||
rspec
|
||||
@ -479,11 +476,11 @@ DEPENDENCIES
|
||||
shoulda
|
||||
sidekiq
|
||||
simple-rss
|
||||
sinatra
|
||||
spork-rails
|
||||
stackprof
|
||||
test_after_commit
|
||||
thor
|
||||
tilt
|
||||
timecop
|
||||
uglifier
|
||||
unf
|
||||
@ -491,4 +488,4 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
BUNDLED WITH
|
||||
1.14.6
|
||||
1.15.1
|
||||
|
||||
@ -2,11 +2,13 @@ import EmailPreview from 'admin/models/email-preview';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
username: null,
|
||||
lastSeen: null,
|
||||
|
||||
emailEmpty: Em.computed.empty('email'),
|
||||
sendEmailDisabled: Em.computed.or('emailEmpty', 'sendingEmail'),
|
||||
showSendEmailForm: Em.computed.notEmpty('model.html_content'),
|
||||
htmlEmpty: Em.computed.empty('model.html_content'),
|
||||
emailEmpty: Ember.computed.empty('email'),
|
||||
sendEmailDisabled: Ember.computed.or('emailEmpty', 'sendingEmail'),
|
||||
showSendEmailForm: Ember.computed.notEmpty('model.html_content'),
|
||||
htmlEmpty: Ember.computed.empty('model.html_content'),
|
||||
|
||||
actions: {
|
||||
refresh() {
|
||||
@ -14,7 +16,14 @@ export default Ember.Controller.extend({
|
||||
|
||||
this.set('loading', true);
|
||||
this.set('sentEmail', false);
|
||||
EmailPreview.findDigest(this.get('lastSeen'), this.get('username')).then(email => {
|
||||
|
||||
let username = this.get('username');
|
||||
if (!username) {
|
||||
username = this.currentUser.get('username');
|
||||
this.set('username', username);
|
||||
}
|
||||
|
||||
EmailPreview.findDigest(username, this.get('lastSeen')).then(email => {
|
||||
model.setProperties(email.getProperties('html_content', 'text_content'));
|
||||
this.set('loading', false);
|
||||
});
|
||||
@ -28,16 +37,14 @@ export default Ember.Controller.extend({
|
||||
this.set('sendingEmail', true);
|
||||
this.set('sentEmail', false);
|
||||
|
||||
const self = this;
|
||||
|
||||
EmailPreview.sendDigest(this.get('lastSeen'), this.get('username'), this.get('email')).then(result => {
|
||||
EmailPreview.sendDigest(this.get('username'), this.get('lastSeen'), this.get('email')).then(result => {
|
||||
if (result.errors) {
|
||||
bootbox.alert(result.errors);
|
||||
} else {
|
||||
self.set('sentEmail', true);
|
||||
this.set('sentEmail', true);
|
||||
}
|
||||
}).catch(popupAjaxError).finally(function() {
|
||||
self.set('sendingEmail', false);
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('sendingEmail', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,15 @@ export default Ember.Controller.extend({
|
||||
];
|
||||
}.property(),
|
||||
|
||||
visibilityLevelOptions: function() {
|
||||
return [
|
||||
{ name: I18n.t("groups.visibility_levels.public"), value: 0 },
|
||||
{ name: I18n.t("groups.visibility_levels.members"), value: 1 },
|
||||
{ name: I18n.t("groups.visibility_levels.staff"), value: 2 },
|
||||
{ name: I18n.t("groups.visibility_levels.owners"), value: 3 }
|
||||
];
|
||||
}.property(),
|
||||
|
||||
trustLevelOptions: function() {
|
||||
return [
|
||||
{ name: I18n.t("groups.trust_levels.none"), value: 0 },
|
||||
@ -22,14 +31,16 @@ export default Ember.Controller.extend({
|
||||
];
|
||||
}.property(),
|
||||
|
||||
@computed('model.visible', 'model.public')
|
||||
disableMembershipRequestSetting(visible, publicGroup) {
|
||||
return !visible || publicGroup;
|
||||
@computed('model.visibility_level', 'model.public')
|
||||
disableMembershipRequestSetting(visibility_level, publicGroup) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return (visibility_level !== 0) || publicGroup;
|
||||
},
|
||||
|
||||
@computed('model.visible', 'model.allow_membership_requests')
|
||||
disablePublicSetting(visible, allowMembershipRequests) {
|
||||
return !visible || allowMembershipRequests;
|
||||
@computed('model.visibility_level', 'model.allow_membership_requests')
|
||||
disablePublicSetting(visibility_level, allowMembershipRequests) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return (visibility_level !== 0) || allowMembershipRequests;
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
@ -1,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;
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
import EmailPreview from 'admin/models/email-preview';
|
||||
import { default as EmailPreview, oneWeekAgo } from 'admin/models/email-preview';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
|
||||
model() {
|
||||
return EmailPreview.findDigest();
|
||||
return EmailPreview.findDigest(this.currentUser.get('username'));
|
||||
},
|
||||
|
||||
afterModel(model) {
|
||||
const controller = this.controllerFor('adminEmailPreviewDigest');
|
||||
controller.setProperties({
|
||||
model: model,
|
||||
lastSeen: moment().subtract(7, 'days').format('YYYY-MM-DD'),
|
||||
model,
|
||||
username: this.currentUser.get('username'),
|
||||
lastSeen: oneWeekAgo(),
|
||||
showHtml: true
|
||||
});
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ export default Discourse.Route.extend({
|
||||
|
||||
model(params) {
|
||||
if (params.name === 'new') {
|
||||
return Group.create({ automatic: false, visible: true });
|
||||
return Group.create({ automatic: false, visibility_level: 0 });
|
||||
}
|
||||
|
||||
const group = this.modelFor('adminGroupsType').findBy('name', params.name);
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<p>{{i18n 'admin.email.preview_digest_desc'}}</p>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<div class='admin-controls email-preview'>
|
||||
<div class='span7 controls'>
|
||||
<label for='last-seen'>{{i18n 'admin.email.last_seen_user'}}</label>
|
||||
{{date-picker-past value=lastSeen id="last-seen"}}
|
||||
<label>{{i18n 'admin.email.user'}}:</label>
|
||||
{{user-selector single="true" usernames=username}}
|
||||
{{user-selector single="true" usernames=username canReceiveUpdates="true"}}
|
||||
<button class='btn' {{action "refresh"}}>{{i18n 'admin.email.refresh'}}</button>
|
||||
<div class="toggle">
|
||||
<label>{{i18n 'admin.email.format'}}</label>
|
||||
|
||||
@ -43,10 +43,8 @@
|
||||
{{/if}}
|
||||
|
||||
<div>
|
||||
<label>
|
||||
{{input type="checkbox" checked=model.visible}}
|
||||
{{i18n 'groups.visible'}}
|
||||
</label>
|
||||
<label for="visiblity">{{i18n 'groups.visibility_levels.title'}}</label>
|
||||
{{combo-box name="alias" valueAttribute="value" value=model.visibility_level content=visibilityLevelOptions}}
|
||||
</div>
|
||||
|
||||
{{#unless model.automatic}}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -228,6 +228,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 +240,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 +266,7 @@ export default Ember.Component.extend({
|
||||
});
|
||||
|
||||
$element.on("fileuploadsend", (e, data) => {
|
||||
this._pasted = false;
|
||||
this._validUploads++;
|
||||
this.appEvents.trigger('composer:insert-text', uploadPlaceholder);
|
||||
|
||||
|
||||
15
app/assets/javascripts/discourse/components/cook-text.js.es6
Normal file
15
app/assets/javascripts/discourse/components/cook-text.js.es6
Normal 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;
|
||||
@ -6,9 +6,9 @@ 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 +78,7 @@ class Toolbar {
|
||||
group: 'insertions',
|
||||
icon: 'quote-right',
|
||||
shortcut: 'Shift+9',
|
||||
perform: e => e.applySurround('> ', '', 'code_text')
|
||||
perform: e => e.applyList('> ', 'blockquote_text')
|
||||
});
|
||||
|
||||
this.addButton({id: 'code', group: 'insertions', shortcut: 'Shift+C', action: 'formatCode'});
|
||||
@ -138,7 +138,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
|
||||
};
|
||||
|
||||
@ -247,6 +247,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 +280,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);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -337,6 +338,10 @@ 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}:`;
|
||||
@ -372,6 +377,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);
|
||||
@ -553,6 +572,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}`;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import { default as computed, on } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Em.Component.extend({
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["date-picker-wrapper"],
|
||||
_picker: null,
|
||||
|
||||
|
||||
@ -0,0 +1,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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import Group from 'discourse/models/group';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
loading: false,
|
||||
|
||||
@computed("model.public")
|
||||
canJoinGroup(publicGroup) {
|
||||
return publicGroup;
|
||||
@ -17,22 +19,6 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@computed
|
||||
disableRequestMembership() {
|
||||
if (this.currentUser) {
|
||||
return this.currentUser.trust_level < this.siteSettings.min_trust_to_send_messages;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("disableRequestMembership")
|
||||
requestMembershipButtonTitle(disableRequestMembership) {
|
||||
if (disableRequestMembership) {
|
||||
return "groups.request_membership_pm.disabled";
|
||||
}
|
||||
},
|
||||
|
||||
_showLoginModal() {
|
||||
this.sendAction('showLogin');
|
||||
$.cookie('destination_url', window.location.href);
|
||||
@ -67,13 +53,12 @@ export default Ember.Component.extend({
|
||||
|
||||
requestMembership() {
|
||||
if (this.currentUser) {
|
||||
const groupName = this.get('model.name');
|
||||
this.set('loading', true);
|
||||
|
||||
Group.loadOwners(groupName).then(result => {
|
||||
const names = result.map(owner => owner.username).join(",");
|
||||
const title = I18n.t('groups.request_membership_pm.title');
|
||||
const body = I18n.t('groups.request_membership_pm.body', { groupName });
|
||||
this.sendAction("createNewMessageViaParams", names, title, body);
|
||||
this.get('model').requestMembership().then(result => {
|
||||
DiscourseURL.routeTo(result.relative_url);
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
} else {
|
||||
this._showLoginModal();
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
export default Ember.Component.extend({
|
||||
actions: {
|
||||
// TODO: When on Ember 1.13, use a closure action
|
||||
loadMore() {
|
||||
this.sendAction('loadMore');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,8 +1,8 @@
|
||||
import { keyDirty } from 'discourse/widgets/widget';
|
||||
import { diff, patch } from 'virtual-dom';
|
||||
import { WidgetClickHook } from 'discourse/widgets/hooks';
|
||||
import { renderedKey, queryRegistry } from 'discourse/widgets/widget';
|
||||
import { queryRegistry } from 'discourse/widgets/widget';
|
||||
import { getRegister } from 'discourse-common/lib/get-owner';
|
||||
import DirtyKeys from 'discourse/lib/dirty-keys';
|
||||
|
||||
const _cleanCallbacks = {};
|
||||
export function addWidgetCleanCallback(widgetName, fn) {
|
||||
@ -18,6 +18,7 @@ export default Ember.Component.extend({
|
||||
_renderCallback: null,
|
||||
_childEvents: null,
|
||||
_dispatched: null,
|
||||
dirtyKeys: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
@ -34,6 +35,7 @@ export default Ember.Component.extend({
|
||||
this._childEvents = [];
|
||||
this._connected = [];
|
||||
this._dispatched = [];
|
||||
this.dirtyKeys = new DirtyKeys(name);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
@ -73,7 +75,7 @@ export default Ember.Component.extend({
|
||||
|
||||
eventDispatched(eventName, key, refreshArg) {
|
||||
const onRefresh = Ember.String.camelize(eventName.replace(/:/, '-'));
|
||||
keyDirty(key, { onRefresh, refreshArg });
|
||||
this.dirtyKeys.keyDirty(key, { onRefresh, refreshArg });
|
||||
this.queueRerender();
|
||||
},
|
||||
|
||||
@ -104,7 +106,10 @@ export default Ember.Component.extend({
|
||||
|
||||
const t0 = new Date().getTime();
|
||||
const args = this.get('args') || this.buildArgs();
|
||||
const opts = { model: this.get('model') };
|
||||
const opts = {
|
||||
model: this.get('model'),
|
||||
dirtyKeys: this.dirtyKeys,
|
||||
};
|
||||
const newTree = new this._widgetClass(args, this.register, opts);
|
||||
|
||||
newTree._rerenderable = this;
|
||||
@ -122,8 +127,8 @@ export default Ember.Component.extend({
|
||||
this._renderCallback = null;
|
||||
}
|
||||
this.afterRender();
|
||||
this.dirtyKeys.renderedKey('*');
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', () => renderedKey('*'));
|
||||
if (this.profileWidget) {
|
||||
console.log(new Date().getTime() - t0);
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { keyDirty } from 'discourse/widgets/widget';
|
||||
import MountWidget from 'discourse/components/mount-widget';
|
||||
import { cloak, uncloak } from 'discourse/widgets/post-stream';
|
||||
import { isWorkaroundActive } from 'discourse/lib/safari-hacks';
|
||||
@ -245,13 +244,13 @@ export default MountWidget.extend({
|
||||
this.appEvents.on('post-stream:refresh', args => {
|
||||
if (args) {
|
||||
if (args.id) {
|
||||
keyDirty(`post-${args.id}`);
|
||||
this.dirtyKeys.keyDirty(`post-${args.id}`);
|
||||
|
||||
if (args.refreshLikes) {
|
||||
keyDirty(`post-menu-${args.id}`, { onRefresh: 'refreshLikes' });
|
||||
this.dirtyKeys.keyDirty(`post-menu-${args.id}`, { onRefresh: 'refreshLikes' });
|
||||
}
|
||||
} else if (args.force) {
|
||||
keyDirty(`*`);
|
||||
this.dirtyKeys.forceAll();
|
||||
}
|
||||
}
|
||||
this.queueRerender();
|
||||
|
||||
@ -77,7 +77,8 @@ export default Em.Component.extend({
|
||||
likes: false,
|
||||
private: false,
|
||||
seen: false
|
||||
}
|
||||
},
|
||||
all_tags: false
|
||||
},
|
||||
status: '',
|
||||
min_post_count: '',
|
||||
@ -230,13 +231,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 +368,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}`);
|
||||
|
||||
@ -2,13 +2,7 @@ import { propertyEqual } from 'discourse/lib/computed';
|
||||
import { actionDescription } from "discourse/components/small-action";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [":item", "item.hidden", "item.deleted", "moderatorAction"],
|
||||
classNameBindings: [":item", "item.hidden", "item.deleted:deleted", "moderatorAction"],
|
||||
moderatorAction: propertyEqual("item.post_type", "site.post_types.moderator_action"),
|
||||
actionDescription: actionDescription("item.action_code", "item.created_at", "item.username"),
|
||||
|
||||
actions: {
|
||||
removeBookmark(userAction) {
|
||||
this.sendAction("removeBookmark", userAction);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import MountWidget from 'discourse/components/mount-widget';
|
||||
|
||||
export default MountWidget.extend({
|
||||
classNames: 'topic-admin-menu-button-container',
|
||||
tagName: 'span',
|
||||
widget: "topic-admin-menu-button",
|
||||
|
||||
buildArgs() {
|
||||
return this.getProperties('topic', 'fixed', 'openUpwards');
|
||||
return this.getProperties('topic', 'fixed', 'openUpwards', 'rightSide');
|
||||
}
|
||||
});
|
||||
|
||||
@ -92,17 +92,18 @@ export default Ember.Component.extend(CleansUp, {
|
||||
this.appEvents.off('topic-entrance:show');
|
||||
},
|
||||
|
||||
_jumpTo(destination) {
|
||||
this.cleanUp();
|
||||
DiscourseURL.routeTo(destination);
|
||||
},
|
||||
|
||||
actions: {
|
||||
enterTop() {
|
||||
const topic = this.get('topic');
|
||||
this.appEvents.trigger('header:update-topic', topic);
|
||||
DiscourseURL.routeTo(topic.get('url'));
|
||||
this._jumpTo(this.get('topic.url'));
|
||||
},
|
||||
|
||||
enterBottom() {
|
||||
const topic = this.get('topic');
|
||||
this.appEvents.trigger('header:update-topic', topic);
|
||||
DiscourseURL.routeTo(topic.get('lastPostUrl'));
|
||||
this._jumpTo(this.get('topic.lastPostUrl'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -20,7 +20,7 @@ export default Ember.Component.extend({
|
||||
|
||||
@computed('postStream.loaded', 'topic.currentPost', 'postStream.filteredPostsCount')
|
||||
hideProgress(loaded, currentPost, filteredPostsCount) {
|
||||
return (!loaded) || (!currentPost) || (filteredPostsCount < 2);
|
||||
return (!loaded) || (!currentPost) || (!this.site.mobileView && filteredPostsCount < 2);
|
||||
},
|
||||
|
||||
@computed('postStream.filteredPostsCount')
|
||||
@ -52,8 +52,14 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
_topicScrolled(event) {
|
||||
this.set('progressPosition', event.postIndex);
|
||||
this._streamPercentage = event.percent;
|
||||
if (this.get('docked')) {
|
||||
this.set('progressPosition', this.get('postStream.filteredPostsCount'));
|
||||
this._streamPercentage = 1.0;
|
||||
} else {
|
||||
this.set('progressPosition', event.postIndex);
|
||||
this._streamPercentage = event.percent;
|
||||
}
|
||||
|
||||
this._updateBar();
|
||||
},
|
||||
|
||||
@ -110,11 +116,10 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
_dock() {
|
||||
const maximumOffset = $('#topic-footer-buttons').offset(),
|
||||
const maximumOffset = $('#topic-bottom').offset(),
|
||||
composerHeight = $('#reply-control').height() || 0,
|
||||
$topicProgressWrapper = this.$(),
|
||||
offset = window.pageYOffset || $('html').scrollTop(),
|
||||
topicProgressHeight = $('#topic-progress').height();
|
||||
offset = window.pageYOffset || $('html').scrollTop();
|
||||
|
||||
if (!$topicProgressWrapper || $topicProgressWrapper.length === 0) {
|
||||
return;
|
||||
@ -124,7 +129,13 @@ export default Ember.Component.extend({
|
||||
if (maximumOffset) {
|
||||
const threshold = maximumOffset.top;
|
||||
const windowHeight = $(window).height();
|
||||
isDocked = offset >= threshold - windowHeight + topicProgressHeight + composerHeight;
|
||||
const headerHeight = $('header').outerHeight(true);
|
||||
|
||||
if (this.capabilities.isIOS) {
|
||||
isDocked = offset >= (threshold - windowHeight - headerHeight + composerHeight);
|
||||
} else {
|
||||
isDocked = offset >= (threshold - windowHeight + composerHeight);
|
||||
}
|
||||
}
|
||||
|
||||
const dockPos = $(document).height() - $('#topic-bottom').offset().top;
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import KeyEnterEscape from 'discourse/mixins/key-enter-escape';
|
||||
|
||||
export default Ember.Component.extend(KeyEnterEscape, {
|
||||
elementId: 'topic-title',
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
import ClickTrack from 'discourse/lib/click-track';
|
||||
import { selectedText } from 'discourse/lib/utilities';
|
||||
import Post from 'discourse/models/post';
|
||||
|
||||
export default Ember.Component.extend(LoadMore, {
|
||||
loading: false,
|
||||
@ -44,6 +45,13 @@ export default Ember.Component.extend(LoadMore, {
|
||||
}.on('willDestroyElement'),
|
||||
|
||||
actions: {
|
||||
removeBookmark(userAction) {
|
||||
const stream = this.get('stream');
|
||||
Post.updateBookmark(userAction.get("post_id"), false).then(() => {
|
||||
stream.remove(userAction);
|
||||
});
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
if (this.get('loading')) { return; }
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import { extractError } from 'discourse/lib/ajax-error';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
offerHelp: null,
|
||||
helpSeen: false,
|
||||
|
||||
@computed('accountEmailOrUsername', 'disabled')
|
||||
submitDisabled(accountEmailOrUsername, disabled) {
|
||||
@ -35,8 +37,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
if (data.user_found === true) {
|
||||
key += '_found';
|
||||
this.set('accountEmailOrUsername', '');
|
||||
bootbox.alert(I18n.t(key, {email: escaped, username: escaped}));
|
||||
this.send("closeModal");
|
||||
this.set('offerHelp', I18n.t(key, {email: escaped, username: escaped}));
|
||||
} else {
|
||||
if (data.user_found === false) {
|
||||
key += '_not_found';
|
||||
@ -52,6 +53,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
ok() {
|
||||
this.send('closeModal');
|
||||
},
|
||||
|
||||
help() {
|
||||
this.setProperties({ offerHelp: I18n.t('forgot_password.help'), helpSeen: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +46,14 @@ export default Ember.Controller.extend({
|
||||
return Em.isEmpty(q);
|
||||
},
|
||||
|
||||
|
||||
@computed('q')
|
||||
highlightQuery(q) {
|
||||
if (!q) { return; }
|
||||
// remove l which can be used for sorting
|
||||
return _.reject(q.split(/\s+/), t => t === 'l').join(' ');
|
||||
},
|
||||
|
||||
@computed('skip_context', 'context')
|
||||
searchContextEnabled: {
|
||||
get(skip,context){
|
||||
|
||||
@ -4,6 +4,9 @@ const _buttons = [];
|
||||
|
||||
const alwaysTrue = () => true;
|
||||
|
||||
function identity() {
|
||||
}
|
||||
|
||||
function addBulkButton(action, key, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
@ -72,7 +75,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
this.perform(operation).then(topics => {
|
||||
if (topics) {
|
||||
topics.forEach(cb);
|
||||
(this.get('refreshClosure') || Ember.k)();
|
||||
(this.get('refreshClosure') || identity)();
|
||||
this.send('closeModal');
|
||||
}
|
||||
});
|
||||
@ -80,7 +83,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
performAndRefresh(operation) {
|
||||
return this.perform(operation).then(() => {
|
||||
(this.get('refreshClosure') || Ember.k)();
|
||||
(this.get('refreshClosure') || identity)();
|
||||
this.send('closeModal');
|
||||
});
|
||||
},
|
||||
@ -145,7 +148,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
this.perform({type: 'change_category', category_id: categoryId}).then(topics => {
|
||||
topics.forEach(t => t.set('category', category));
|
||||
(this.get('refreshClosure') || Ember.k)();
|
||||
(this.get('refreshClosure') || identity)();
|
||||
this.send('closeModal');
|
||||
});
|
||||
},
|
||||
|
||||
@ -196,7 +196,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
const quotedText = Quote.build(post, buffer);
|
||||
composerOpts.quote = quotedText;
|
||||
if (composer.get('model.viewOpen')) {
|
||||
this.appEvents.trigger('composer:insert-text', quotedText);
|
||||
this.appEvents.trigger('composer:insert-block', quotedText);
|
||||
} else if (composer.get('model.viewDraft')) {
|
||||
const model = composer.get('model');
|
||||
model.set('reply', model.get('reply') + quotedText);
|
||||
@ -320,7 +320,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
composerController.get('content.action') === Composer.REPLY) {
|
||||
composerController.set('content.post', post);
|
||||
composerController.set('content.composeState', Composer.OPEN);
|
||||
this.appEvents.trigger('composer:insert-text', quotedText.trim());
|
||||
this.appEvents.trigger('composer:insert-block', quotedText.trim());
|
||||
} else {
|
||||
|
||||
const opts = {
|
||||
|
||||
@ -12,6 +12,7 @@ export default Ember.Controller.extend({
|
||||
canLoadMore: true,
|
||||
invitesLoading: false,
|
||||
reinvitedAll: false,
|
||||
rescindedAll: false,
|
||||
|
||||
init: function() {
|
||||
this._super();
|
||||
@ -32,7 +33,7 @@ export default Ember.Controller.extend({
|
||||
|
||||
inviteRedeemed: Em.computed.equal('filter', 'redeemed'),
|
||||
|
||||
showReinviteAllButton: function() {
|
||||
showBulkActionButtons: function() {
|
||||
return (this.get('filter') === "pending" && this.get('model').invites.length > 4 && this.currentUser.get('staff'));
|
||||
}.property('filter'),
|
||||
|
||||
@ -86,17 +87,27 @@ export default Ember.Controller.extend({
|
||||
return false;
|
||||
},
|
||||
|
||||
rescindAll() {
|
||||
bootbox.confirm(I18n.t("user.invited.rescind_all_confirm"), confirm => {
|
||||
if (confirm) {
|
||||
Invite.rescindAll().then(() => {
|
||||
this.set('rescindedAll', true);
|
||||
this.get('model.invites').clear();
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
reinvite(invite) {
|
||||
invite.reinvite();
|
||||
return false;
|
||||
},
|
||||
|
||||
reinviteAll() {
|
||||
const self = this;
|
||||
bootbox.confirm(I18n.t("user.invited.reinvite_all_confirm"), confirm => {
|
||||
if (confirm) {
|
||||
Invite.reinviteAll().then(function() {
|
||||
self.set('reinvitedAll', true);
|
||||
Invite.reinviteAll().then(() => {
|
||||
this.set('reinvitedAll', true);
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
import { cook } from 'discourse/lib/text';
|
||||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||
|
||||
registerUnbound('cook-text', cook);
|
||||
@ -2,5 +2,5 @@
|
||||
|
||||
export default {
|
||||
name: "inject-objects",
|
||||
initialize: Ember.K
|
||||
initialize() { }
|
||||
};
|
||||
|
||||
@ -2,5 +2,5 @@
|
||||
|
||||
export default {
|
||||
name: "register-discourse-location",
|
||||
initialize: Ember.K
|
||||
initialize() { }
|
||||
};
|
||||
|
||||
@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
32
app/assets/javascripts/discourse/lib/dirty-keys.js.es6
Normal file
32
app/assets/javascripts/discourse/lib/dirty-keys.js.es6
Normal 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
@ -1,6 +1,6 @@
|
||||
import groups from 'discourse/lib/emoji/groups';
|
||||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import { emojiList } from 'pretty-text/emoji';
|
||||
import { emojiList, isSkinTonableEmoji } from 'pretty-text/emoji';
|
||||
import { emojiUrlFor } from 'discourse/lib/text';
|
||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||
|
||||
@ -11,6 +11,7 @@ let PER_ROW = 12;
|
||||
const PER_PAGE = 60;
|
||||
|
||||
let ungroupedIcons, recentlyUsedIcons;
|
||||
let selectedSkinTone = keyValueStore.getObject('selectedSkinTone') || 1;
|
||||
|
||||
if (!keyValueStore.getObject(EMOJI_USAGE)) {
|
||||
keyValueStore.setObject({key: EMOJI_USAGE, value: {}});
|
||||
@ -121,6 +122,13 @@ function bindEvents(page, offset, options) {
|
||||
render(p, 0, options);
|
||||
return false;
|
||||
});
|
||||
|
||||
$('.emoji-modal .tones-button').click(function(){
|
||||
selectedSkinTone = parseInt($(this).data('skin-tone'));
|
||||
keyValueStore.setObject({key: 'selectedSkinTone', value: selectedSkinTone});
|
||||
render(page, offset, options);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function render(page, offset, options) {
|
||||
@ -139,13 +147,30 @@ function render(page, offset, options) {
|
||||
rows.push(row);
|
||||
row = [];
|
||||
}
|
||||
row.push({src: emojiUrlFor(icons[i]), title: icons[i]});
|
||||
|
||||
let code = icons[i];
|
||||
if(selectedSkinTone !== 1 && isSkinTonableEmoji(code)) {
|
||||
code = `${code}:t${selectedSkinTone}`;
|
||||
}
|
||||
|
||||
row.push({src: emojiUrlFor(code), title: code});
|
||||
}
|
||||
rows.push(row);
|
||||
|
||||
const skinTones = [];
|
||||
const skinToneNames = ['default', 'light', 'medium-light', 'medium', 'medium-dark', 'dark'];
|
||||
for(let i=1; i<skinToneNames.length+1; i++){
|
||||
skinTones.push({
|
||||
selected: selectedSkinTone === i,
|
||||
level: i,
|
||||
className: skinToneNames[i-1]
|
||||
});
|
||||
}
|
||||
|
||||
const model = {
|
||||
toolbarItems: toolbarItems,
|
||||
rows: rows,
|
||||
toolbarItems,
|
||||
skinTones,
|
||||
rows,
|
||||
prevDisabled: offset === 0,
|
||||
nextDisabled: (max + 1) > icons.length,
|
||||
modalClass: options.modalClass
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -2,24 +2,39 @@ 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));
|
||||
} else {
|
||||
return Ember.RSVP.Promise.resolve(cook(text));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function sanitize(text, options) {
|
||||
return textSanitize(text, new WhiteLister(options));
|
||||
}
|
||||
|
||||
function emojiOptions() {
|
||||
|
||||
@ -221,6 +221,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 +372,7 @@ const DiscourseURL = Ember.Object.extend({
|
||||
discoveryTopics.resetParams();
|
||||
}
|
||||
|
||||
router.router.updateURL(path);
|
||||
router._routerMicrolib.updateURL(path);
|
||||
}
|
||||
|
||||
const split = path.split('#');
|
||||
|
||||
@ -172,7 +172,7 @@ export function validateUploadedFiles(files, opts) {
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
opts["type"] = uploadTypeFromFileName(upload.name);
|
||||
opts.type = uploadTypeFromFileName(upload.name);
|
||||
|
||||
return validateUploadedFile(upload, opts);
|
||||
}
|
||||
@ -185,12 +185,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 +208,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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import Post from 'discourse/models/post';
|
||||
|
||||
export default Post.extend({
|
||||
|
||||
_attachCategory: function () {
|
||||
const categoryId = this.get("category_id");
|
||||
if (categoryId) {
|
||||
this.set("category", Discourse.Category.findById(categoryId));
|
||||
}
|
||||
}.on("init"),
|
||||
|
||||
presentName: Ember.computed.or('name', 'username'),
|
||||
|
||||
sameUser: function() {
|
||||
return this.get("username") === Discourse.User.currentProp("username");
|
||||
}.property("username"),
|
||||
|
||||
descriptionKey: function () {
|
||||
if (this.get("reply_to_post_number")) {
|
||||
return this.get("sameUser") ? "you_replied_to_post" : "user_replied_to_post";
|
||||
} else {
|
||||
return this.get("sameUser") ? "you_replied_to_topic" : "user_replied_to_topic";
|
||||
}
|
||||
}.property("reply_to_post_number", "sameUser")
|
||||
|
||||
});
|
||||
@ -2,7 +2,6 @@ import { ajax } from 'discourse/lib/ajax';
|
||||
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
|
||||
import GroupHistory from 'discourse/models/group-history';
|
||||
import RestModel from 'discourse/models/rest';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
const Group = RestModel.extend({
|
||||
limit: 50,
|
||||
@ -114,23 +113,27 @@ const Group = RestModel.extend({
|
||||
return aliasLevel === '99';
|
||||
},
|
||||
|
||||
@observes("visible", "canEveryoneMention")
|
||||
@observes("visibility_level", "canEveryoneMention")
|
||||
_updateAllowMembershipRequests() {
|
||||
if (!this.get('visible') || !this.get('canEveryoneMention')) {
|
||||
if (this.get('visibility_level') !== 0 || !this.get('canEveryoneMention')) {
|
||||
this.set ('allow_membership_requests', false);
|
||||
}
|
||||
},
|
||||
|
||||
@observes("visible")
|
||||
@observes("visibility_level")
|
||||
_updatePublic() {
|
||||
if (!this.get('visible')) this.set('public', false);
|
||||
let visibility_level = parseInt(this.get('visibility_level'));
|
||||
if (visibility_level !== 0) {
|
||||
this.set('public', false);
|
||||
this.set('allow_membership_requests', false);
|
||||
}
|
||||
},
|
||||
|
||||
asJSON() {
|
||||
return {
|
||||
name: this.get('name'),
|
||||
alias_level: this.get('alias_level'),
|
||||
visible: !!this.get('visible'),
|
||||
visibility_level: this.get('visibility_level'),
|
||||
automatic_membership_email_domains: this.get('emailDomains'),
|
||||
automatic_membership_retroactive: !!this.get('automatic_membership_retroactive'),
|
||||
title: this.get('title'),
|
||||
@ -202,7 +205,13 @@ 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({
|
||||
@ -216,10 +225,6 @@ Group.reopenClass({
|
||||
return ajax("/groups/" + name + ".json").then(result => Group.create(result.basic_group));
|
||||
},
|
||||
|
||||
loadOwners(name) {
|
||||
return ajax('/groups/' + name + '/owners.json').catch(popupAjaxError);
|
||||
},
|
||||
|
||||
loadMembers(name, offset, limit, params) {
|
||||
return ajax('/groups/' + name + '/members.json', {
|
||||
data: _.extend({
|
||||
|
||||
@ -58,6 +58,10 @@ Invite.reopenClass({
|
||||
|
||||
reinviteAll() {
|
||||
return ajax('/invites/reinvite-all', { type: 'POST' });
|
||||
},
|
||||
|
||||
rescindAll() {
|
||||
return ajax('/invites/rescind-all', { type: 'POST' });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@ const RestModel = Ember.Object.extend({
|
||||
isCreated: Ember.computed.equal('__state', 'created'),
|
||||
isSaving: false,
|
||||
|
||||
afterUpdate: Ember.K,
|
||||
afterUpdate() { },
|
||||
|
||||
update(props) {
|
||||
if (this.get('isSaving')) { return Ember.RSVP.reject(); }
|
||||
|
||||
@ -297,7 +297,16 @@ export default Ember.Object.extend({
|
||||
|
||||
if (existing) {
|
||||
delete obj.id;
|
||||
const klass = this.register.lookupFactory('model:' + type) || RestModel;
|
||||
let klass = this.register.lookupFactory('model:' + type);
|
||||
|
||||
if (klass && klass.class) {
|
||||
klass = klass.class;
|
||||
}
|
||||
|
||||
if (!klass) {
|
||||
klass = RestModel;
|
||||
}
|
||||
|
||||
existing.setProperties(klass.munge(obj));
|
||||
obj.id = id;
|
||||
return existing;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import AdminPost from 'discourse/models/admin-post';
|
||||
import UserAction from 'discourse/models/user-action';
|
||||
|
||||
export default Discourse.Model.extend({
|
||||
loaded: false,
|
||||
@ -36,7 +36,7 @@ export default Discourse.Model.extend({
|
||||
|
||||
return ajax(this.get("url"), { cache: false }).then(function (result) {
|
||||
if (result) {
|
||||
const posts = result.map(function (post) { return AdminPost.create(post); });
|
||||
const posts = result.map(function (post) { return UserAction.create(post); });
|
||||
self.get("content").pushObjects(posts);
|
||||
self.setProperties({
|
||||
loaded: true,
|
||||
|
||||
@ -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' });
|
||||
},
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@ export default Discourse.Route.extend({
|
||||
model(params) {
|
||||
if (PreloadStore.get("invite_info")) {
|
||||
return PreloadStore.getAndRemove("invite_info").then(json => _.merge(params, json));
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -218,6 +218,10 @@ const TopicRoute = Discourse.Route.extend({
|
||||
|
||||
// We reset screen tracking every time a topic is entered
|
||||
this.screenTrack.start(model.get('id'), controller);
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
this.appEvents.trigger('header:update-topic', model);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -19,26 +19,9 @@ export default Discourse.Route.extend(ViewingActionType, {
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
didTransition() {
|
||||
this.controllerFor("user-activity")._showFooter();
|
||||
return true;
|
||||
},
|
||||
|
||||
removeBookmark(userAction) {
|
||||
var user = this.modelFor("user");
|
||||
Discourse.Post.updateBookmark(userAction.get("post_id"), false)
|
||||
.then(function() {
|
||||
// remove the user action from the stream
|
||||
user.get("stream").remove(userAction);
|
||||
// update the counts
|
||||
user.get("stats").forEach(function (stat) {
|
||||
if (stat.get("action_type") === userAction.action_type) {
|
||||
stat.decrementProperty("count");
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1 @@
|
||||
{{cooked}}
|
||||
@ -2,5 +2,8 @@
|
||||
{{fa-icon icon}}
|
||||
{{/if}}
|
||||
|
||||
{{{translatedLabel}}}
|
||||
{{#if translatedLabel}}
|
||||
<span class='d-button-label'>{{{translatedLabel}}}</span>
|
||||
{{/if}}
|
||||
|
||||
{{yield}}
|
||||
|
||||
@ -30,5 +30,6 @@
|
||||
<div class="d-editor-preview">
|
||||
{{{preview}}}
|
||||
</div>
|
||||
{{plugin-outlet name="editor-preview"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1 +1 @@
|
||||
{{input type="text" class="date-picker" placeholder=placeholder}}
|
||||
{{input type="text" class="date-picker" placeholder=placeholder value=value}}
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
{{#if item.truncated}}
|
||||
<a class="expand-item" href {{action "expandItem"}} title={{i18n "post.expand_collapse"}}>
|
||||
{{fa-icon "chevron-down"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
@ -24,10 +24,13 @@
|
||||
{{else}}
|
||||
{{d-button action="requestMembership"
|
||||
class="group-index-request"
|
||||
disabled=loading
|
||||
icon="envelope"
|
||||
label="groups.request"
|
||||
title=requestMembershipButtonTitle
|
||||
disabled=disableRequestMembership}}
|
||||
label="groups.request"}}
|
||||
|
||||
{{#if loading}}
|
||||
{{loading-spinner size="small"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{yield}}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
{{#load-more selector=".user-stream .item" action="loadMore"}}
|
||||
<div class='user-stream'>
|
||||
{{#each posts as |post|}}
|
||||
{{group-post post=post}}
|
||||
{{else}}
|
||||
<div>{{i18n emptyText}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/load-more}}
|
||||
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
@ -2,6 +2,7 @@
|
||||
<div class='clearfix info'>
|
||||
<a href="{{unbound post.user.userUrl}}" data-user-card="{{unbound post.user.username}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar post.user imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
|
||||
<span class='time'>{{format-date post.created_at leaveAgo="true"}}</span>
|
||||
{{expand-post item=post}}
|
||||
<span class="title">
|
||||
<a href={{post.url}}>{{{post.topic.fancyTitle}}}</a>
|
||||
</span>
|
||||
@ -13,6 +14,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class='excerpt'>
|
||||
{{{unbound post.excerpt}}}
|
||||
{{{post.excerpt}}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
{{#if editing}}
|
||||
{{d-editor value=buffered.raw}}
|
||||
{{else}}
|
||||
{{{cook-text post.raw}}}
|
||||
{{cook-text post.raw}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
||||
@ -41,6 +41,9 @@
|
||||
<label class="control-label" for="search-with-tags">{{i18n "search.advanced.with_tags.label"}}</label>
|
||||
<div class="controls">
|
||||
{{tag-chooser tags=searchedTerms.tags blacklist=searchedTerms.tags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true" width="70%"}}
|
||||
<section class="field">
|
||||
<label>{{ input type="checkbox" class="all-tags" checked=searchedTerms.special.all_tags}} {{i18n "search.advanced.filters.all_tags"}} </label>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,21 @@
|
||||
<div class='clearfix info'>
|
||||
<a href={{item.userUrl}} data-user-card={{item.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
|
||||
<span class='time'>{{format-date item.created_at}}</span>
|
||||
{{expand-post item=item}}
|
||||
{{topic-status topic=item disableActions=true}}
|
||||
<span class="title">
|
||||
<a href={{item.postUrl}}>{{{item.title}}}</a>
|
||||
</span>
|
||||
<div class="category">{{category-link item.category}}</div>
|
||||
|
||||
{{#if item.deleted_by}}
|
||||
<span class="delete-info">
|
||||
{{fa-icon "trash-o"}}
|
||||
{{avatar item.deleted_by imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}
|
||||
{{format-date item.deleted_at leaveAgo="true"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="user-stream-item-header" args=(hash item=item)}}
|
||||
</div>
|
||||
|
||||
@ -20,7 +30,7 @@
|
||||
<i class="icon {{child.icon}}"></i>
|
||||
{{#each child.items as |grandChild|}}
|
||||
{{#if grandChild.removableBookmark}}
|
||||
<button class="btn btn-default remove-bookmark" {{action "removeBookmark" grandChild}}>
|
||||
<button class="btn btn-default remove-bookmark" {{action removeBookmark grandChild}}>
|
||||
{{fa-icon 'times'}} {{i18n "bookmarks.remove"}}
|
||||
</button>
|
||||
{{else}}
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
{{#unless hideProgress}}
|
||||
{{yield}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if showBackButton}}
|
||||
<div class='progress-back-container'>
|
||||
{{d-button label="topic.timeline.back" class="btn-primary progress-back" action="goBack" icon="arrow-down"}}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
<div class="container">
|
||||
<div class="title-wrapper">
|
||||
{{yield}}
|
||||
</div>
|
||||
{{plugin-outlet name="topic-title" args=(hash model=model)}}
|
||||
</div>
|
||||
@ -0,0 +1,3 @@
|
||||
{{#each stream.content as |item|}}
|
||||
{{stream-item item=item removeBookmark=(action "removeBookmark")}}
|
||||
{{/each}}
|
||||
@ -14,22 +14,32 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='info'></div>
|
||||
<div class='nav'>
|
||||
<span class='prev'>
|
||||
{{#if prevDisabled}}
|
||||
{{fa-icon "fast-backward"}}
|
||||
{{else}}
|
||||
<a>{{fa-icon "fast-backward"}}</a>
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class='next'>
|
||||
{{#if nextDisabled}}
|
||||
{{fa-icon "fast-forward"}}
|
||||
{{else}}
|
||||
<a>{{fa-icon "fast-forward"}}</a>
|
||||
{{/if}}
|
||||
</span>
|
||||
<div class='footer'>
|
||||
<div class='info'></div>
|
||||
<div class='tones'>
|
||||
{{#each skinTones as |skinTone|}}
|
||||
<a href='#' class='tones-button {{skinTone.className}}' data-skin-tone="{{skinTone.level}}">
|
||||
{{#if skinTone.selected}}{{fa-icon "check"}}{{/if}}
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<span class='prev'>
|
||||
{{#if prevDisabled}}
|
||||
{{fa-icon "fast-backward"}}
|
||||
{{else}}
|
||||
<a>{{fa-icon "fast-backward"}}</a>
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class='next'>
|
||||
{{#if nextDisabled}}
|
||||
{{fa-icon "fast-forward"}}
|
||||
{{else}}
|
||||
<a>{{fa-icon "fast-forward"}}</a>
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='clearfix'></div>
|
||||
</div>
|
||||
|
||||
@ -107,7 +107,7 @@
|
||||
</span>
|
||||
|
||||
{{#if result.blurb}}
|
||||
{{#highlight-text highlight=q}}
|
||||
{{#highlight-text highlight=highlightQuery}}
|
||||
{{{unbound result.blurb}}}
|
||||
{{/highlight-text}}
|
||||
{{/if}}
|
||||
|
||||
@ -1 +1,11 @@
|
||||
{{group-post-stream posts=model emptyText=emptyText loadMore="loadMore" loading=loading}}
|
||||
{{#load-more selector=".user-stream .item" action=(action "loadMore")}}
|
||||
<div class='user-stream'>
|
||||
{{#each model as |post|}}
|
||||
{{group-post post=post}}
|
||||
{{else}}
|
||||
<div>{{i18n emptyText}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/load-more}}
|
||||
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
|
||||
@ -43,9 +43,7 @@
|
||||
{{/each}}
|
||||
{{/mobile-nav}}
|
||||
|
||||
{{group-membership-button model=model
|
||||
createNewMessageViaParams='createNewMessageViaParams'
|
||||
showLogin='showLogin'}}
|
||||
{{group-membership-button model=model showLogin='showLogin'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -45,7 +45,6 @@
|
||||
|
||||
<td>
|
||||
{{#group-membership-button model=group
|
||||
createNewMessageViaParams='createNewMessageViaParams'
|
||||
showMembershipStatus=true
|
||||
groupUserIds=groups.extras.group_user_ids
|
||||
showLogin='showLogin'}}
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
{{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn}}
|
||||
{{input-tip validation=passwordValidation}}
|
||||
<div class="instructions">
|
||||
{{passwordInstructions}}
|
||||
{{passwordInstructions}} {{i18n 'invites.optional_description'}}
|
||||
<div class="caps-lock-warning {{unless capsLockOn 'invisible'}}"><i class="fa fa-exclamation-triangle"></i> {{i18n 'login.caps_lock_warning'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,27 @@
|
||||
<form>
|
||||
{{#d-modal-body}}
|
||||
<label for='username-or-email'>{{i18n 'forgot_password.invite'}}</label>
|
||||
{{text-field value=accountEmailOrUsername placeholderKey="login.email_placeholder" id="username-or-email" autocorrect="off" autocapitalize="off"}}
|
||||
{{#d-modal-body class="forgot-password-modal"}}
|
||||
{{#unless offerHelp}}
|
||||
<label for='username-or-email'>{{i18n 'forgot_password.invite'}}</label>
|
||||
{{text-field value=accountEmailOrUsername placeholderKey="login.email_placeholder" id="username-or-email" autocorrect="off" autocapitalize="off"}}
|
||||
{{else}}
|
||||
{{{offerHelp}}}
|
||||
{{/unless}}
|
||||
{{/d-modal-body}}
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-large btn-primary' disabled={{submitDisabled}} {{action "submit"}}>{{i18n 'forgot_password.reset'}}</button>
|
||||
{{#unless offerHelp}}
|
||||
{{d-button action="submit"
|
||||
label="forgot_password.reset"
|
||||
disabled=submitDisabled
|
||||
class="btn-primary"}}
|
||||
{{else}}
|
||||
{{d-button class="btn-large btn-primary"
|
||||
label="forgot_password.button_ok"
|
||||
action="ok"}}
|
||||
{{#unless helpSeen}}
|
||||
{{d-button class="btn-large"
|
||||
label="forgot_password.button_help"
|
||||
action="help"}}
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -7,6 +7,11 @@
|
||||
{{categories-admin-dropdown}}
|
||||
{{/if}}
|
||||
{{#if canCreateTopic}}
|
||||
<button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button>
|
||||
{{d-button
|
||||
id="create-topic"
|
||||
action="createTopic"
|
||||
icon="plus"
|
||||
label="topic.create"
|
||||
}}
|
||||
{{/if}}
|
||||
{{/d-section}}
|
||||
|
||||
@ -4,6 +4,6 @@
|
||||
{{navigation-bar navItems=navItems filterMode=filterMode}}
|
||||
|
||||
{{#if canCreateTopic}}
|
||||
<button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button>
|
||||
<button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i><span>{{i18n 'topic.create'}}</span></button>
|
||||
{{/if}}
|
||||
{{/d-section}}
|
||||
|
||||
@ -10,59 +10,53 @@
|
||||
|
||||
{{#if model.postStream.loaded}}
|
||||
{{#if model.postStream.firstPostPresent}}
|
||||
<div id="topic-title">
|
||||
<div class="container">
|
||||
{{#topic-title cancelled="cancelEditingTopic" save="finishedEditingTopic"}}
|
||||
{{#if editingTopic}}
|
||||
{{#if model.isPrivateMessage}}
|
||||
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
|
||||
{{/if}}
|
||||
|
||||
<div class="title-wrapper">
|
||||
{{#if editingTopic}}
|
||||
{{#if model.isPrivateMessage}}
|
||||
{{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}}
|
||||
{{#if showCategoryChooser}}
|
||||
<br>
|
||||
{{category-chooser valueAttribute="id" value=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canEditTags}}
|
||||
<br>
|
||||
{{tag-chooser tags=buffered.tags categoryId=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}}
|
||||
|
||||
<br>
|
||||
{{d-button action="finishedEditingTopic" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelEditingTopic" class="btn-small cancel-edit" icon="times"}}
|
||||
{{else}}
|
||||
<h1>
|
||||
{{#unless model.is_warning}}
|
||||
<a href={{pmPath}}>
|
||||
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/unless}}
|
||||
|
||||
{{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}}
|
||||
{{#if showCategoryChooser}}
|
||||
<br>
|
||||
{{category-chooser valueAttribute="id" value=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canEditTags}}
|
||||
<br>
|
||||
{{tag-chooser tags=buffered.tags categoryId=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}}
|
||||
|
||||
<br>
|
||||
{{d-button action="finishedEditingTopic" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelEditingTopic" class="btn-small cancel-edit" icon="times"}}
|
||||
{{else}}
|
||||
<h1>
|
||||
{{#unless model.is_warning}}
|
||||
<a href={{pmPath}}>
|
||||
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
|
||||
</a>
|
||||
{{/unless}}
|
||||
|
||||
{{#if model.details.loaded}}
|
||||
{{topic-status topic=model}}
|
||||
<a href="{{unbound model.url}}" {{action "jumpTop"}} class="fancy-title">
|
||||
{{{model.fancyTitle}}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.details.can_edit}}
|
||||
<a href {{action "editTopic"}} class="edit-topic" title="{{i18n "edit"}}">{{fa-icon "pencil"}}</a>
|
||||
{{/if}}
|
||||
</h1>
|
||||
|
||||
{{#unless model.isPrivateMessage}}
|
||||
{{topic-category topic=model class="topic-category"}}
|
||||
{{/unless}}
|
||||
{{#if model.details.loaded}}
|
||||
{{topic-status topic=model}}
|
||||
<a href="{{unbound model.url}}" {{action "jumpTop"}} class="fancy-title">
|
||||
{{{model.fancyTitle}}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{plugin-outlet name="topic-title" args=(hash model=model)}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if model.details.can_edit}}
|
||||
<a href {{action "editTopic"}} class="edit-topic" title="{{i18n "edit"}}">{{fa-icon "pencil"}}</a>
|
||||
{{/if}}
|
||||
</h1>
|
||||
|
||||
{{#unless model.isPrivateMessage}}
|
||||
{{topic-category topic=model class="topic-category"}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{/topic-title}}
|
||||
{{/if}}
|
||||
|
||||
<div class="container posts">
|
||||
@ -71,24 +65,24 @@
|
||||
</div>
|
||||
|
||||
{{#topic-navigation topic=model jumpToIndex=(action "jumpToIndex") as |info|}}
|
||||
{{#if info.renderAdminMenuButton}}
|
||||
{{topic-admin-menu-button
|
||||
topic=model
|
||||
fixed="true"
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(action "topicRouteAction" "showTopicStatusUpdate")
|
||||
showFeatureTopic=(action "topicRouteAction" "showFeatureTopic")
|
||||
showChangeTimestamp=(action "topicRouteAction" "showChangeTimestamp")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{/if}}
|
||||
|
||||
{{#if info.renderTimeline}}
|
||||
{{#if info.renderAdminMenuButton}}
|
||||
{{topic-admin-menu-button
|
||||
topic=model
|
||||
fixed="true"
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(action "topicRouteAction" "showTopicStatusUpdate")
|
||||
showFeatureTopic=(action "topicRouteAction" "showFeatureTopic")
|
||||
showChangeTimestamp=(action "topicRouteAction" "showChangeTimestamp")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{/if}}
|
||||
|
||||
{{topic-timeline
|
||||
topic=model
|
||||
prevEvent=info.prevEvent
|
||||
@ -113,11 +107,29 @@
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{else}}
|
||||
{{topic-progress
|
||||
{{#topic-progress
|
||||
prevEvent=info.prevEvent
|
||||
topic=model
|
||||
expanded=info.topicProgressExpanded
|
||||
jumpToPost=(action "jumpToPost")}}
|
||||
{{#if info.renderAdminMenuButton}}
|
||||
{{topic-admin-menu-button
|
||||
topic=model
|
||||
openUpwards="true"
|
||||
rightSide="true"
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(action "topicRouteAction" "showTopicStatusUpdate")
|
||||
showFeatureTopic=(action "topicRouteAction" "showFeatureTopic")
|
||||
showChangeTimestamp=(action "topicRouteAction" "showChangeTimestamp")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{/if}}
|
||||
{{/topic-progress}}
|
||||
{{/if}}
|
||||
{{/topic-navigation}}
|
||||
|
||||
|
||||
@ -19,7 +19,12 @@
|
||||
{{csv-uploader uploading=uploading}}
|
||||
<a href="https://meta.discourse.org/t/sending-bulk-user-invites/16468" target="_blank" style="color:black;">{{fa-icon "question-circle"}}</a>
|
||||
{{/if}}
|
||||
{{#if showReinviteAllButton}}
|
||||
{{#if showBulkActionButtons}}
|
||||
{{#if rescindedAll}}
|
||||
{{i18n 'user.invited.rescinded_all'}}
|
||||
{{else}}
|
||||
{{d-button icon="times" action="rescindAll" class="btn" label="user.invited.rescind_all"}}
|
||||
{{/if}}
|
||||
{{#if reinvitedAll}}
|
||||
{{i18n 'user.invited.reinvited_all'}}
|
||||
{{else}}
|
||||
|
||||
@ -1,30 +1 @@
|
||||
{{#user-stream stream=model}}
|
||||
{{#each model.content as |p|}}
|
||||
<div class="item {{if p.hidden 'hidden'}} {{if p.deleted 'deleted'}} {{if p.moderator_action 'moderator-action'}}">
|
||||
<div class="clearfix info">
|
||||
<a href="{{unbound p.usernameUrl}}" class="avatar-link">
|
||||
<div class="avatar-wrapper">
|
||||
{{avatar p imageSize="large" extraClasses="actor" ignoreTitle="true"}}
|
||||
</div>
|
||||
</a>
|
||||
<span class="time">
|
||||
{{format-date p.created_at leaveAgo="true"}}
|
||||
</span>
|
||||
<span class="title">
|
||||
<a href="{{unbound p.url}}">{{unbound p.topic_title}}</a>
|
||||
</span>
|
||||
<span class="category">
|
||||
{{category-link p.category}}
|
||||
</span>
|
||||
{{#if p.deleted}}
|
||||
<span class="delete-info">
|
||||
<i class="fa fa-trash-o"></i> {{avatar p.deleted_by imageSize="tiny" extraClasses="actor" ignoreTitle="true"}} {{format-date p.deleted_at leaveAgo="true"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<p class="excerpt">
|
||||
{{{p.excerpt}}}
|
||||
</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/user-stream}}
|
||||
{{user-stream stream=model}}
|
||||
|
||||
@ -3,8 +3,4 @@
|
||||
{{{model.noContentHelp}}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#user-stream stream=model}}
|
||||
{{#each model.content as |item|}}
|
||||
{{stream-item item=item removeBookmark="removeBookmark"}}
|
||||
{{/each}}
|
||||
{{/user-stream}}
|
||||
{{user-stream stream=model}}
|
||||
|
||||
@ -139,7 +139,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='top-section'>
|
||||
<div class='top-section most-liked-section'>
|
||||
<div class='top-sub-section likes-section pull-left'>
|
||||
<h3 class='stats-title'>{{i18n "user.summary.most_liked_by"}}</h3>
|
||||
{{#if model.most_liked_by_users.length}}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { diff, patch } from 'virtual-dom';
|
||||
import { queryRegistry } from 'discourse/widgets/widget';
|
||||
import DirtyKeys from 'discourse/lib/dirty-keys';
|
||||
|
||||
export default class WidgetGlue {
|
||||
|
||||
@ -9,6 +10,7 @@ export default class WidgetGlue {
|
||||
this.register = register;
|
||||
this.attrs = attrs;
|
||||
this._timeout = null;
|
||||
this.dirtyKeys = new DirtyKeys(name);
|
||||
|
||||
this._widgetClass = queryRegistry(name) || this.register.lookupFactory(`widget:${name}`);
|
||||
if (!this._widgetClass) {
|
||||
@ -27,7 +29,11 @@ export default class WidgetGlue {
|
||||
|
||||
rerenderWidget() {
|
||||
Ember.run.cancel(this._timeout);
|
||||
const newTree = new this._widgetClass(this.attrs, this.register);
|
||||
const newTree = new this._widgetClass(
|
||||
this.attrs,
|
||||
this.register,
|
||||
{ dirtyKeys: this.dirtyKeys }
|
||||
);
|
||||
const patches = diff(this._tree || this._rootNode, newTree);
|
||||
|
||||
newTree._rerenderable = this;
|
||||
|
||||
@ -282,7 +282,10 @@ export default createWidget('header', {
|
||||
},
|
||||
|
||||
toggleUserMenu() {
|
||||
this.state.ringBackdrop = false;
|
||||
if (this.currentUser.get('read_first_notification')) {
|
||||
this.state.ringBackdrop = false;
|
||||
};
|
||||
|
||||
this.state.userVisible = !this.state.userVisible;
|
||||
},
|
||||
|
||||
|
||||
@ -11,12 +11,12 @@ export default createWidget('link', {
|
||||
const route = attrs.route;
|
||||
if (route) {
|
||||
const router = this.register.lookup('router:main');
|
||||
if (router && router.router) {
|
||||
if (router && router._routerMicrolib) {
|
||||
const params = [route];
|
||||
if (attrs.model) {
|
||||
params.push(attrs.model);
|
||||
}
|
||||
return Discourse.getURL(router.router.generate.apply(router.router, params));
|
||||
return Discourse.getURL(router._routerMicrolib.generate.apply(router._routerMicrolib, params));
|
||||
}
|
||||
} else {
|
||||
return Discourse.getURL(attrs.href);
|
||||
|
||||
@ -86,6 +86,35 @@ registerButton('edit', attrs => {
|
||||
}
|
||||
});
|
||||
|
||||
registerButton('reply-small', attrs => {
|
||||
if (!attrs.canCreatePost) { return; }
|
||||
|
||||
const args = {
|
||||
action: 'replyToPost',
|
||||
title: 'post.controls.reply',
|
||||
icon: 'reply',
|
||||
className: 'reply',
|
||||
};
|
||||
|
||||
return args;
|
||||
});
|
||||
|
||||
registerButton('wiki-edit', attrs => {
|
||||
if (attrs.canEdit) {
|
||||
const args = {
|
||||
action: 'editPost',
|
||||
className: 'edit create',
|
||||
title: 'post.controls.edit',
|
||||
icon: 'pencil-square-o',
|
||||
alwaysShowYours: true
|
||||
};
|
||||
if (!attrs.mobileView) {
|
||||
args.label = 'post.controls.edit_action';
|
||||
}
|
||||
return args;
|
||||
}
|
||||
});
|
||||
|
||||
registerButton('replies', (attrs, state, siteSettings) => {
|
||||
const replyCount = attrs.replyCount;
|
||||
|
||||
@ -180,6 +209,13 @@ registerButton('delete', attrs => {
|
||||
}
|
||||
});
|
||||
|
||||
function replaceButton(buttons, find, replace) {
|
||||
const idx = buttons.indexOf(find);
|
||||
if (idx !== -1) {
|
||||
buttons[idx] = replace;
|
||||
}
|
||||
}
|
||||
|
||||
export default createWidget('post-menu', {
|
||||
tagName: 'section.post-menu-area.clearfix',
|
||||
|
||||
@ -209,7 +245,16 @@ export default createWidget('post-menu', {
|
||||
|
||||
const allButtons = [];
|
||||
let visibleButtons = [];
|
||||
siteSettings.post_menu.split('|').forEach(i => {
|
||||
|
||||
const orderedButtons = siteSettings.post_menu.split('|');
|
||||
|
||||
// If the post is a wiki, make Edit more prominent
|
||||
if (attrs.wiki) {
|
||||
replaceButton(orderedButtons, 'edit', 'reply-small');
|
||||
replaceButton(orderedButtons, 'reply', 'wiki-edit');
|
||||
}
|
||||
|
||||
orderedButtons.forEach(i => {
|
||||
const button = this.attachButton(i, attrs);
|
||||
if (button) {
|
||||
allButtons.push(button);
|
||||
|
||||
@ -2,7 +2,6 @@ import { createWidget } from 'discourse/widgets/widget';
|
||||
import transformPost from 'discourse/lib/transform-post';
|
||||
import { Placeholder } from 'discourse/lib/posts-with-placeholders';
|
||||
import { addWidgetCleanCallback } from 'discourse/components/mount-widget';
|
||||
import { keyDirty } from 'discourse/widgets/widget';
|
||||
|
||||
let transformCallbacks = null;
|
||||
function postTransformCallbacks(transformed) {
|
||||
@ -36,14 +35,15 @@ export function cloak(post, component) {
|
||||
const $post = $(`#post_${post.post_number}`);
|
||||
_cloaked[post.id] = true;
|
||||
_heights[post.id] = $post.outerHeight();
|
||||
keyDirty(`post-${post.id}`);
|
||||
|
||||
component.dirtyKeys.keyDirty(`post-${post.id}`);
|
||||
Ember.run.debounce(component, 'queueRerender', 1000);
|
||||
}
|
||||
|
||||
export function uncloak(post, component) {
|
||||
if (!CLOAKING_ENABLED || !_cloaked[post.id]) { return; }
|
||||
_cloaked[post.id] = null;
|
||||
keyDirty(`post-${post.id}`);
|
||||
component.dirtyKeys.keyDirty(`post-${post.id}`);
|
||||
component.queueRerender();
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,9 @@ class Highlighted extends RawHtml {
|
||||
|
||||
decorate($html) {
|
||||
if (this.term) {
|
||||
$html.highlight(this.term.split(/\s+/), { className: 'search-highlight' });
|
||||
// special case ignore "l" which is used for magic sorting
|
||||
const words = _.reject(this.term.split(/\s+/), t => t === 'l');
|
||||
$html.highlight(words, { className: 'search-highlight' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,19 +28,26 @@ createWidget('topic-admin-menu-button', {
|
||||
if (!this.currentUser || !this.currentUser.get('canManageTopic')) { return; }
|
||||
|
||||
const result = [];
|
||||
result.push(this.attach('button', {
|
||||
className: 'btn ' + (attrs.fixed ? " show-topic-admin" : ""),
|
||||
title: 'topic_admin_menu',
|
||||
icon: 'wrench',
|
||||
action: 'showAdminMenu',
|
||||
sendActionEvent: true
|
||||
}));
|
||||
|
||||
// We don't show the button when expanded on the right side
|
||||
if (!(attrs.rightSide && state.expanded)) {
|
||||
result.push(this.attach('button', {
|
||||
className: 'btn ' + (attrs.fixed ? " show-topic-admin" : ""),
|
||||
title: 'topic_admin_menu',
|
||||
icon: 'wrench',
|
||||
action: 'showAdminMenu',
|
||||
sendActionEvent: true
|
||||
}));
|
||||
}
|
||||
|
||||
if (state.expanded) {
|
||||
result.push(this.attach('topic-admin-menu', { position: state.position,
|
||||
fixed: attrs.fixed,
|
||||
topic: attrs.topic,
|
||||
openUpwards: attrs.openUpwards }));
|
||||
result.push(this.attach('topic-admin-menu', {
|
||||
position: state.position,
|
||||
fixed: attrs.fixed,
|
||||
topic: attrs.topic,
|
||||
openUpwards: attrs.openUpwards,
|
||||
rightSide: attrs.rightSide
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -69,10 +76,20 @@ createWidget('topic-admin-menu-button', {
|
||||
export default createWidget('topic-admin-menu', {
|
||||
tagName: 'div.popup-menu.topic-admin-popup-menu',
|
||||
|
||||
buildClasses(attrs) {
|
||||
if (attrs.rightSide) {
|
||||
return 'right-side';
|
||||
}
|
||||
},
|
||||
|
||||
buildAttributes(attrs) {
|
||||
const { top, left, outerHeight } = attrs.position;
|
||||
let { top, left, outerHeight } = attrs.position;
|
||||
const position = attrs.fixed ? 'fixed' : 'absolute';
|
||||
|
||||
if (attrs.rightSide) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attrs.openUpwards) {
|
||||
const documentHeight = $(document).height();
|
||||
const mainHeight = $('#main').height();
|
||||
|
||||
@ -9,21 +9,6 @@ import DecoratorHelper from 'discourse/widgets/decorator-helper';
|
||||
function emptyContent() { }
|
||||
|
||||
const _registry = {};
|
||||
let _dirty = {};
|
||||
|
||||
export function keyDirty(key, options) {
|
||||
options = options || {};
|
||||
options.dirty = true;
|
||||
_dirty[key] = options;
|
||||
}
|
||||
|
||||
export function renderedKey(key) {
|
||||
if (key === '*') {
|
||||
_dirty = {};
|
||||
} else {
|
||||
delete _dirty[key];
|
||||
}
|
||||
}
|
||||
|
||||
export function queryRegistry(name) {
|
||||
return _registry[name];
|
||||
@ -149,6 +134,7 @@ export default class Widget {
|
||||
this.mergeState = opts.state;
|
||||
this.model = opts.model;
|
||||
this.register = register;
|
||||
this.dirtyKeys = opts.dirtyKeys;
|
||||
|
||||
register.deprecateContainer(this);
|
||||
|
||||
@ -188,6 +174,8 @@ export default class Widget {
|
||||
}
|
||||
|
||||
render(prev) {
|
||||
const { dirtyKeys } = this;
|
||||
|
||||
if (prev && prev.key && prev.key === this.key) {
|
||||
this.state = prev.state;
|
||||
} else {
|
||||
@ -200,16 +188,16 @@ export default class Widget {
|
||||
}
|
||||
|
||||
if (prev) {
|
||||
const dirtyOpts = _dirty[prev.key] || { dirty: false };
|
||||
const dirtyOpts = dirtyKeys.optionsFor(prev.key);
|
||||
|
||||
if (prev.shadowTree) {
|
||||
this.shadowTree = true;
|
||||
if (!dirtyOpts.dirty && !_dirty['*']) {
|
||||
if (!dirtyOpts.dirty && !dirtyKeys.allDirty()) {
|
||||
return prev.vnode;
|
||||
}
|
||||
}
|
||||
if (prev.key) {
|
||||
renderedKey(prev.key);
|
||||
dirtyKeys.renderedKey(prev.key);
|
||||
}
|
||||
|
||||
const refreshAction = dirtyOpts.onRefresh;
|
||||
@ -248,11 +236,15 @@ export default class Widget {
|
||||
return;
|
||||
}
|
||||
WidgetClass = this.register.lookupFactory(`widget:${widgetName}`);
|
||||
if (WidgetClass && WidgetClass.class) {
|
||||
WidgetClass = WidgetClass.class;
|
||||
}
|
||||
}
|
||||
|
||||
if (WidgetClass) {
|
||||
const result = new WidgetClass(attrs, this.register, opts);
|
||||
result.parentWidget = this;
|
||||
result.dirtyKeys = this.dirtyKeys;
|
||||
return result;
|
||||
} else {
|
||||
throw `Couldn't find ${widgetName} factory`;
|
||||
@ -263,7 +255,7 @@ export default class Widget {
|
||||
let widget = this;
|
||||
while (widget) {
|
||||
if (widget.shadowTree) {
|
||||
keyDirty(widget.key);
|
||||
this.dirtyKeys.keyDirty(widget.key);
|
||||
}
|
||||
|
||||
const rerenderable = widget._rerenderable;
|
||||
|
||||
14
app/assets/javascripts/markdown-it-bundle.js
Normal file
14
app/assets/javascripts/markdown-it-bundle.js
Normal file
@ -0,0 +1,14 @@
|
||||
//= require markdown-it.js
|
||||
//= require ./pretty-text/engines/markdown-it/helpers
|
||||
//= require ./pretty-text/engines/markdown-it/mentions
|
||||
//= require ./pretty-text/engines/markdown-it/quotes
|
||||
//= require ./pretty-text/engines/markdown-it/emoji
|
||||
//= require ./pretty-text/engines/markdown-it/onebox
|
||||
//= require ./pretty-text/engines/markdown-it/bbcode-block
|
||||
//= require ./pretty-text/engines/markdown-it/bbcode-inline
|
||||
//= require ./pretty-text/engines/markdown-it/code
|
||||
//= require ./pretty-text/engines/markdown-it/category-hashtag
|
||||
//= require ./pretty-text/engines/markdown-it/censored
|
||||
//= require ./pretty-text/engines/markdown-it/table
|
||||
//= require ./pretty-text/engines/markdown-it/paragraph
|
||||
//= require ./pretty-text/engines/markdown-it/newline
|
||||
@ -4,6 +4,7 @@
|
||||
//= require ./pretty-text/emoji/data
|
||||
//= require ./pretty-text/emoji
|
||||
//= require ./pretty-text/engines/discourse-markdown
|
||||
//= require ./pretty-text/engines/discourse-markdown-it
|
||||
//= require_tree ./pretty-text/engines/discourse-markdown
|
||||
//= require xss.min
|
||||
//= require better_markdown.js
|
||||
|
||||
@ -2,9 +2,11 @@ function escapeRegexp(text) {
|
||||
return text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
export function censor(text, censoredWords, censoredPattern) {
|
||||
let patterns = [],
|
||||
originalText = text;
|
||||
export function censorFn(censoredWords, censoredPattern, replacementLetter) {
|
||||
|
||||
let patterns = [];
|
||||
|
||||
replacementLetter = replacementLetter || "■";
|
||||
|
||||
if (censoredWords && censoredWords.length) {
|
||||
patterns = censoredWords.split("|").map(t => `(${escapeRegexp(t)})`);
|
||||
@ -21,19 +23,35 @@ export function censor(text, censoredWords, censoredPattern) {
|
||||
censorRegexp = new RegExp("(\\b(?:" + patterns.join("|") + ")\\b)(?![^\\(]*\\))", "ig");
|
||||
|
||||
if (censorRegexp) {
|
||||
let m = censorRegexp.exec(text);
|
||||
|
||||
while (m && m[0]) {
|
||||
if (m[0].length > originalText.length) { return originalText; } // regex is dangerous
|
||||
const replacement = new Array(m[0].length+1).join('■');
|
||||
text = text.replace(new RegExp(`(\\b${escapeRegexp(m[0])}\\b)(?![^\\(]*\\))`, "ig"), replacement);
|
||||
m = censorRegexp.exec(text);
|
||||
}
|
||||
return function(text) {
|
||||
let original = text;
|
||||
|
||||
try {
|
||||
let m = censorRegexp.exec(text);
|
||||
|
||||
while (m && m[0]) {
|
||||
if (m[0].length > original.length) { return original; } // regex is dangerous
|
||||
const replacement = new Array(m[0].length+1).join(replacementLetter);
|
||||
text = text.replace(new RegExp(`(\\b${escapeRegexp(m[0])}\\b)(?![^\\(]*\\))`, "ig"), replacement);
|
||||
m = censorRegexp.exec(text);
|
||||
}
|
||||
|
||||
return text;
|
||||
} catch (e) {
|
||||
return original;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
} catch(e) {
|
||||
return originalText;
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
return function(t){ return t;};
|
||||
}
|
||||
|
||||
export function censor(text, censoredWords, censoredPattern, replacementLetter) {
|
||||
return censorFn(censoredWords, censoredPattern, replacementLetter)(text);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { emoji, aliases, translations } from 'pretty-text/emoji/data';
|
||||
import { emojis, aliases, translations, tonableEmojis } from 'pretty-text/emoji/data';
|
||||
|
||||
// bump up this number to expire all emojis
|
||||
export const IMAGE_VERSION = "3";
|
||||
export const IMAGE_VERSION = "5";
|
||||
|
||||
const extendedEmoji = {};
|
||||
|
||||
@ -11,7 +11,7 @@ export function registerEmoji(code, url) {
|
||||
}
|
||||
|
||||
export function emojiList() {
|
||||
const result = emoji.slice(0);
|
||||
const result = emojis.slice(0);
|
||||
_.each(extendedEmoji, (v,k) => result.push(k));
|
||||
return result;
|
||||
}
|
||||
@ -19,7 +19,7 @@ export function emojiList() {
|
||||
const emojiHash = {};
|
||||
|
||||
// add all default emojis
|
||||
emoji.forEach(code => emojiHash[code] = true);
|
||||
emojis.forEach(code => emojiHash[code] = true);
|
||||
|
||||
// and their aliases
|
||||
const aliasHash = {};
|
||||
@ -30,7 +30,7 @@ Object.keys(aliases).forEach(name => {
|
||||
export function performEmojiUnescape(string, opts) {
|
||||
// this can be further improved by supporting matches of emoticons that don't begin with a colon
|
||||
if (string.indexOf(":") >= 0) {
|
||||
return string.replace(/\B:[^\s:]+:?\B/g, m => {
|
||||
return string.replace(/\B:[^\s:]+(?::t\d)?:?\B/g, m => {
|
||||
const isEmoticon = !!translations[m];
|
||||
const emojiVal = isEmoticon ? translations[m] : m.slice(1, m.length - 1);
|
||||
const hasEndingColon = m.lastIndexOf(":") === m.length - 1;
|
||||
@ -64,8 +64,9 @@ export function buildEmojiUrl(code, opts) {
|
||||
url = opts.customEmoji[code];
|
||||
}
|
||||
|
||||
if (!url && emojiHash.hasOwnProperty(code)) {
|
||||
url = opts.getURL(`/images/emoji/${opts.emojiSet}/${code}.png`);
|
||||
const noToneMatch = code.match(/(.?[\w-]*)?:?/);
|
||||
if (noToneMatch && !url && (emojiHash.hasOwnProperty(noToneMatch[1]) || aliasHash.hasOwnProperty(noToneMatch[1]))) {
|
||||
url = opts.getURL(`/images/emoji/${opts.emojiSet}/${code.replace(/:t/, '/')}.png`);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
@ -77,7 +78,7 @@ export function buildEmojiUrl(code, opts) {
|
||||
|
||||
export function emojiExists(code) {
|
||||
code = code.toLowerCase();
|
||||
return !!(extendedEmoji.hasOwnProperty(code) || emojiHash.hasOwnProperty(code));
|
||||
return !!(extendedEmoji.hasOwnProperty(code) || emojiHash.hasOwnProperty(code) || aliasHash.hasOwnProperty(code));
|
||||
};
|
||||
|
||||
let toSearch;
|
||||
@ -113,3 +114,12 @@ export function emojiSearch(term, options) {
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export function isSkinTonableEmoji(term) {
|
||||
let match = term.match(/^:?(.*?):?$/);
|
||||
if (match) {
|
||||
return tonableEmojis.indexOf(match[1]) !== -1;
|
||||
} else {
|
||||
return tonableEmojis.indexOf(term) !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export const emoji = <%= Emoji.standard.map(&:name).flatten.inspect %>;
|
||||
export const emojis = <%= Emoji.standard.map(&:name).flatten.inspect %>;
|
||||
export const tonableEmojis = <%= Emoji.tonable_emojis.flatten.inspect %>;
|
||||
export const aliases = <%= Emoji.aliases.inspect.gsub("=>", ":") %>;
|
||||
export const translations = {
|
||||
':)' : 'slight_smile',
|
||||
|
||||
@ -0,0 +1,212 @@
|
||||
import { default as WhiteLister, whiteListFeature } from 'pretty-text/white-lister';
|
||||
import { sanitize } from 'pretty-text/sanitizer';
|
||||
import guid from 'pretty-text/guid';
|
||||
|
||||
function deprecate(feature, name){
|
||||
return function() {
|
||||
if (window.console && window.console.log) {
|
||||
window.console.log(feature + ': ' + name + ' is deprecated, please use the new markdown it APIs');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createHelper(featureName, opts, optionCallbacks, pluginCallbacks, getOptions) {
|
||||
let helper = {};
|
||||
helper.markdownIt = true;
|
||||
helper.whiteList = info => whiteListFeature(featureName, info);
|
||||
helper.registerInline = deprecate(featureName,'registerInline');
|
||||
helper.replaceBlock = deprecate(featureName,'replaceBlock');
|
||||
helper.addPreProcessor = deprecate(featureName,'addPreProcessor');
|
||||
helper.inlineReplace = deprecate(featureName,'inlineReplace');
|
||||
helper.postProcessTag = deprecate(featureName,'postProcessTag');
|
||||
helper.inlineRegexp = deprecate(featureName,'inlineRegexp');
|
||||
helper.inlineBetween = deprecate(featureName,'inlineBetween');
|
||||
helper.postProcessText = deprecate(featureName,'postProcessText');
|
||||
helper.onParseNode = deprecate(featureName,'onParseNode');
|
||||
helper.registerBlock = deprecate(featureName,'registerBlock');
|
||||
// hack to allow moving of getOptions
|
||||
helper.getOptions = () => getOptions.f();
|
||||
|
||||
helper.registerOptions = (callback) => {
|
||||
optionCallbacks.push([featureName, callback]);
|
||||
};
|
||||
|
||||
helper.registerPlugin = (callback) => {
|
||||
pluginCallbacks.push([featureName, callback]);
|
||||
};
|
||||
|
||||
return helper;
|
||||
}
|
||||
|
||||
// TODO we may just use a proper ruler from markdown it... this is a basic proxy
|
||||
class Ruler {
|
||||
constructor() {
|
||||
this.rules = [];
|
||||
}
|
||||
|
||||
getRules() {
|
||||
return this.rules;
|
||||
}
|
||||
|
||||
getRuleForTag(tag) {
|
||||
this.ensureCache();
|
||||
return this.cache[tag];
|
||||
}
|
||||
|
||||
ensureCache() {
|
||||
if (this.cache) { return; }
|
||||
|
||||
this.cache = {};
|
||||
for(let i=this.rules.length-1;i>=0;i--) {
|
||||
let info = this.rules[i];
|
||||
this.cache[info.rule.tag] = info;
|
||||
}
|
||||
}
|
||||
|
||||
push(name, rule) {
|
||||
this.rules.push({name, rule});
|
||||
this.cache = null;
|
||||
}
|
||||
}
|
||||
|
||||
// block bb code ruler for parsing of quotes / code / polls
|
||||
function setupBlockBBCode(md) {
|
||||
md.block.bbcode_ruler = new Ruler();
|
||||
}
|
||||
|
||||
function setupInlineBBCode(md) {
|
||||
md.inline.bbcode_ruler = new Ruler();
|
||||
}
|
||||
|
||||
function renderHoisted(tokens, idx, options) {
|
||||
const content = tokens[idx].content;
|
||||
if (content && content.length > 0) {
|
||||
let id = guid();
|
||||
options.discourse.hoisted[id] = tokens[idx].content;
|
||||
return id;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function setupUrlDecoding(md) {
|
||||
// this fixed a subtle issue where %20 is decoded as space in
|
||||
// automatic urls
|
||||
md.utils.lib.mdurl.decode.defaultChars = ';/?:@&=+$,# ';
|
||||
}
|
||||
|
||||
function setupHoister(md) {
|
||||
md.renderer.rules.html_raw = renderHoisted;
|
||||
}
|
||||
|
||||
export function setup(opts, siteSettings, state) {
|
||||
if (opts.setup) {
|
||||
return;
|
||||
}
|
||||
|
||||
opts.markdownIt = true;
|
||||
|
||||
let optionCallbacks = [];
|
||||
let pluginCallbacks = [];
|
||||
|
||||
// ideally I would like to change the top level API a bit, but in the mean time this will do
|
||||
let getOptions = {
|
||||
f: () => opts
|
||||
};
|
||||
|
||||
const check = /discourse-markdown\/|markdown-it\//;
|
||||
let features = [];
|
||||
|
||||
Object.keys(require._eak_seen).forEach(entry => {
|
||||
if (check.test(entry)) {
|
||||
const module = require(entry);
|
||||
if (module && module.setup) {
|
||||
|
||||
const featureName = entry.split('/').reverse()[0];
|
||||
features.push(featureName);
|
||||
module.setup(createHelper(featureName, opts, optionCallbacks, pluginCallbacks, getOptions));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
optionCallbacks.forEach(([,callback])=>{
|
||||
callback(opts, siteSettings, state);
|
||||
});
|
||||
|
||||
// enable all features by default
|
||||
features.forEach(feature => {
|
||||
if (!opts.features.hasOwnProperty(feature)) {
|
||||
opts.features[feature] = true;
|
||||
}
|
||||
});
|
||||
|
||||
let copy = {};
|
||||
Object.keys(opts).forEach(entry => {
|
||||
copy[entry] = opts[entry];
|
||||
delete opts[entry];
|
||||
});
|
||||
|
||||
opts.discourse = copy;
|
||||
getOptions.f = () => opts.discourse;
|
||||
|
||||
opts.engine = window.markdownit({
|
||||
discourse: opts.discourse,
|
||||
html: true,
|
||||
breaks: opts.discourse.features.newline,
|
||||
xhtmlOut: false,
|
||||
linkify: true,
|
||||
typographer: siteSettings.enable_markdown_typographer
|
||||
});
|
||||
|
||||
setupUrlDecoding(opts.engine);
|
||||
setupHoister(opts.engine);
|
||||
setupBlockBBCode(opts.engine);
|
||||
setupInlineBBCode(opts.engine);
|
||||
|
||||
pluginCallbacks.forEach(([feature, callback])=>{
|
||||
if (opts.discourse.features[feature]) {
|
||||
opts.engine.use(callback);
|
||||
}
|
||||
});
|
||||
|
||||
// top level markdown it notifier
|
||||
opts.markdownIt = true;
|
||||
opts.setup = true;
|
||||
|
||||
if (!opts.discourse.sanitizer) {
|
||||
const whiteLister = new WhiteLister(opts.discourse);
|
||||
opts.sanitizer = opts.discourse.sanitizer = (!!opts.discourse.sanitize) ? a=>sanitize(a, whiteLister) : a=>a;
|
||||
}
|
||||
}
|
||||
|
||||
export function cook(raw, opts) {
|
||||
// we still have to hoist html_raw nodes so they bypass the whitelister
|
||||
// this is the case for oneboxes
|
||||
let hoisted = {};
|
||||
|
||||
opts.discourse.hoisted = hoisted;
|
||||
|
||||
const rendered = opts.engine.render(raw);
|
||||
let cooked = opts.discourse.sanitizer(rendered).trim();
|
||||
|
||||
const keys = Object.keys(hoisted);
|
||||
if (keys.length) {
|
||||
let found = true;
|
||||
|
||||
const unhoist = function(key) {
|
||||
cooked = cooked.replace(new RegExp(key, "g"), function() {
|
||||
found = true;
|
||||
return hoisted[key];
|
||||
});
|
||||
};
|
||||
|
||||
while (found) {
|
||||
found = false;
|
||||
keys.forEach(unhoist);
|
||||
}
|
||||
}
|
||||
|
||||
delete opts.discourse.hoisted;
|
||||
return cooked;
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user