Version bump

This commit is contained in:
Neil Lalonde 2017-07-05 12:24:00 -04:00
commit 3989a0d9f9
17480 changed files with 34515 additions and 14243 deletions

View File

@ -6,28 +6,27 @@
"browser": true,
"builtin": true
},
"ecmaVersion": 7,
"parserOptions": {
"ecmaVersion": 7,
"sourceType": "module"
},
"globals":
{"Ember":true,
"jQuery":true,
"$":true,
"QUnit":true,
"RSVP":true,
"Discourse":true,
"Em":true,
"Handlebars":true,
"I18n":true,
"bootbox":true,
"module":true,
"moduleFor":true,
"moduleForComponent":true,
"Pretender":true,
"sandbox":true,
"controllerFor":true,
"test":true,
"ok":true,
"not":true,
"expect":true,
"equal":true,
"visit":true,
"andThen":true,
"click":true,
@ -48,12 +47,8 @@
"find":true,
"sinon":true,
"moment":true,
"start":true,
"_":true,
"alert":true,
"containsInstance":true,
"deepEqual":true,
"notEqual":true,
"define":true,
"require":true,
"requirejs":true,

View File

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

View File

@ -36,6 +36,7 @@ end
gem 'mail'
gem 'mime-types', require: 'mime/types/columnar'
gem 'mini_mime'
gem 'hiredis'
gem 'redis', require: ["redis", "redis/connection/hiredis"]
@ -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

View File

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

View File

@ -2,11 +2,13 @@ import EmailPreview from 'admin/models/email-preview';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend({
username: null,
lastSeen: null,
emailEmpty: Em.computed.empty('email'),
sendEmailDisabled: Em.computed.or('emailEmpty', 'sendingEmail'),
showSendEmailForm: Em.computed.notEmpty('model.html_content'),
htmlEmpty: Em.computed.empty('model.html_content'),
emailEmpty: Ember.computed.empty('email'),
sendEmailDisabled: Ember.computed.or('emailEmpty', 'sendingEmail'),
showSendEmailForm: Ember.computed.notEmpty('model.html_content'),
htmlEmpty: Ember.computed.empty('model.html_content'),
actions: {
refresh() {
@ -14,7 +16,14 @@ export default Ember.Controller.extend({
this.set('loading', true);
this.set('sentEmail', false);
EmailPreview.findDigest(this.get('lastSeen'), this.get('username')).then(email => {
let username = this.get('username');
if (!username) {
username = this.currentUser.get('username');
this.set('username', username);
}
EmailPreview.findDigest(username, this.get('lastSeen')).then(email => {
model.setProperties(email.getProperties('html_content', 'text_content'));
this.set('loading', false);
});
@ -28,16 +37,14 @@ export default Ember.Controller.extend({
this.set('sendingEmail', true);
this.set('sentEmail', false);
const self = this;
EmailPreview.sendDigest(this.get('lastSeen'), this.get('username'), this.get('email')).then(result => {
EmailPreview.sendDigest(this.get('username'), this.get('lastSeen'), this.get('email')).then(result => {
if (result.errors) {
bootbox.alert(result.errors);
} else {
self.set('sentEmail', true);
this.set('sentEmail', true);
}
}).catch(popupAjaxError).finally(function() {
self.set('sendingEmail', false);
}).catch(popupAjaxError).finally(() => {
this.set('sendingEmail', false);
});
}
}

View File

@ -15,6 +15,15 @@ export default Ember.Controller.extend({
];
}.property(),
visibilityLevelOptions: function() {
return [
{ name: I18n.t("groups.visibility_levels.public"), value: 0 },
{ name: I18n.t("groups.visibility_levels.members"), value: 1 },
{ name: I18n.t("groups.visibility_levels.staff"), value: 2 },
{ name: I18n.t("groups.visibility_levels.owners"), value: 3 }
];
}.property(),
trustLevelOptions: function() {
return [
{ name: I18n.t("groups.trust_levels.none"), value: 0 },
@ -22,14 +31,16 @@ export default Ember.Controller.extend({
];
}.property(),
@computed('model.visible', 'model.public')
disableMembershipRequestSetting(visible, publicGroup) {
return !visible || publicGroup;
@computed('model.visibility_level', 'model.public')
disableMembershipRequestSetting(visibility_level, publicGroup) {
visibility_level = parseInt(visibility_level);
return (visibility_level !== 0) || publicGroup;
},
@computed('model.visible', 'model.allow_membership_requests')
disablePublicSetting(visible, allowMembershipRequests) {
return !visible || allowMembershipRequests;
@computed('model.visibility_level', 'model.allow_membership_requests')
disablePublicSetting(visibility_level, allowMembershipRequests) {
visibility_level = parseInt(visibility_level);
return (visibility_level !== 0) || allowMembershipRequests;
},
actions: {

View File

@ -1,42 +1,24 @@
import { ajax } from 'discourse/lib/ajax';
const EmailPreview = Discourse.Model.extend({});
export function oneWeekAgo() {
return moment().locale('en').subtract(7, 'days').format('YYYY-MM-DD');
}
EmailPreview.reopenClass({
findDigest: function(lastSeenAt, username) {
if (Em.isEmpty(lastSeenAt)) {
lastSeenAt = this.oneWeekAgo();
}
if (Em.isEmpty(username)) {
username = Discourse.User.current().username;
}
findDigest(username, lastSeenAt) {
return ajax("/admin/email/preview-digest.json", {
data: { last_seen_at: lastSeenAt, username: username }
}).then(function (result) {
return EmailPreview.create(result);
});
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username }
}).then(result => EmailPreview.create(result));
},
sendDigest: function(lastSeenAt, username, email) {
if (Em.isEmpty(lastSeenAt)) {
lastSeenAt = this.oneWeekAgo();
}
if (Em.isEmpty(username)) {
username = Discourse.User.current().username;
}
sendDigest(username, lastSeenAt, email) {
return ajax("/admin/email/send-digest.json", {
data: { last_seen_at: lastSeenAt, username: username, email: email }
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username, email }
});
},
oneWeekAgo() {
const en = moment().locale('en');
return en.subtract(7, 'days').format('YYYY-MM-DD');
}
});
export default EmailPreview;

View File

@ -1,16 +1,17 @@
import EmailPreview from 'admin/models/email-preview';
import { default as EmailPreview, oneWeekAgo } from 'admin/models/email-preview';
export default Discourse.Route.extend({
model() {
return EmailPreview.findDigest();
return EmailPreview.findDigest(this.currentUser.get('username'));
},
afterModel(model) {
const controller = this.controllerFor('adminEmailPreviewDigest');
controller.setProperties({
model: model,
lastSeen: moment().subtract(7, 'days').format('YYYY-MM-DD'),
model,
username: this.currentUser.get('username'),
lastSeen: oneWeekAgo(),
showHtml: true
});
}

View File

@ -4,7 +4,7 @@ export default Discourse.Route.extend({
model(params) {
if (params.name === 'new') {
return Group.create({ automatic: false, visible: true });
return Group.create({ automatic: false, visibility_level: 0 });
}
const group = this.modelFor('adminGroupsType').findBy('name', params.name);

View File

@ -1,11 +1,11 @@
<p>{{i18n 'admin.email.preview_digest_desc'}}</p>
<div class='admin-controls'>
<div class='admin-controls email-preview'>
<div class='span7 controls'>
<label for='last-seen'>{{i18n 'admin.email.last_seen_user'}}</label>
{{date-picker-past value=lastSeen id="last-seen"}}
<label>{{i18n 'admin.email.user'}}:</label>
{{user-selector single="true" usernames=username}}
{{user-selector single="true" usernames=username canReceiveUpdates="true"}}
<button class='btn' {{action "refresh"}}>{{i18n 'admin.email.refresh'}}</button>
<div class="toggle">
<label>{{i18n 'admin.email.format'}}</label>

View File

@ -43,10 +43,8 @@
{{/if}}
<div>
<label>
{{input type="checkbox" checked=model.visible}}
{{i18n 'groups.visible'}}
</label>
<label for="visiblity">{{i18n 'groups.visibility_levels.title'}}</label>
{{combo-box name="alias" valueAttribute="value" value=model.visibility_level content=visibilityLevelOptions}}
</div>
{{#unless model.automatic}}

View File

@ -15,7 +15,11 @@ export function getRegister(obj) {
const register = {
lookup: (...args) => owner.lookup(...args),
lookupFactory: (...args) => {
return owner.lookupFactory ? owner.lookupFactory(...args) : owner._lookupFactory(...args);
if (owner.factoryFor) {
return owner.factoryFor(...args);
} else if (owner._lookupFactory) {
return owner._lookupFactory(...args);
}
},
deprecateContainer(target) {

View File

@ -3,8 +3,9 @@ import Composer from 'discourse/models/composer';
import afterTransition from 'discourse/lib/after-transition';
import positioningWorkaround from 'discourse/lib/safari-hacks';
import { headerHeight } from 'discourse/components/site-header';
import KeyEnterEscape from 'discourse/mixins/key-enter-escape';
export default Ember.Component.extend({
export default Ember.Component.extend(KeyEnterEscape, {
elementId: 'reply-control',
classNameBindings: ['composer.creatingPrivateMessage:private-message',
@ -65,17 +66,6 @@ export default Ember.Component.extend({
}, 1000);
},
keyDown(e) {
if (e.which === 27) {
this.sendAction('cancelled');
return false;
} else if (e.which === 13 && (e.ctrlKey || e.metaKey)) {
// CTRL+ENTER or CMD+ENTER
this.sendAction('save');
return false;
}
},
@observes('composeState')
disableFullscreen() {
if (this.get('composeState') !== Composer.OPEN && positioningWorkaround.blur) {

View File

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

View File

@ -0,0 +1,15 @@
import { cookAsync } from 'discourse/lib/text';
const CookText = Ember.Component.extend({
tagName: '',
cooked: null,
didReceiveAttrs() {
this._super(...arguments);
cookAsync(this.get('rawText')).then(cooked => this.set('cooked', cooked));
}
});
CookText.reopenClass({ positionalParams: ['rawText'] });
export default CookText;

View File

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

View File

@ -2,7 +2,7 @@
import loadScript from "discourse/lib/load-script";
import { default as computed, on } from "ember-addons/ember-computed-decorators";
export default Em.Component.extend({
export default Ember.Component.extend({
classNames: ["date-picker-wrapper"],
_picker: null,

View File

@ -0,0 +1,19 @@
import { ajax } from 'discourse/lib/ajax';
export default Ember.Component.extend({
tagName: '',
actions: {
expandItem() {
const item = this.get('item');
const topicId = item.get('topic_id');
const postNumber = item.get('post_number');
return ajax(`/posts/by_number/${topicId}/${postNumber}.json`).then(result => {
item.set('truncated', false);
item.set('excerpt', result.cooked);
});
}
}
});

View File

@ -1,8 +1,10 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import Group from 'discourse/models/group';
import DiscourseURL from 'discourse/lib/url';
export default Ember.Component.extend({
loading: false,
@computed("model.public")
canJoinGroup(publicGroup) {
return publicGroup;
@ -17,22 +19,6 @@ export default Ember.Component.extend({
}
},
@computed
disableRequestMembership() {
if (this.currentUser) {
return this.currentUser.trust_level < this.siteSettings.min_trust_to_send_messages;
} else {
return false;
}
},
@computed("disableRequestMembership")
requestMembershipButtonTitle(disableRequestMembership) {
if (disableRequestMembership) {
return "groups.request_membership_pm.disabled";
}
},
_showLoginModal() {
this.sendAction('showLogin');
$.cookie('destination_url', window.location.href);
@ -67,13 +53,12 @@ export default Ember.Component.extend({
requestMembership() {
if (this.currentUser) {
const groupName = this.get('model.name');
this.set('loading', true);
Group.loadOwners(groupName).then(result => {
const names = result.map(owner => owner.username).join(",");
const title = I18n.t('groups.request_membership_pm.title');
const body = I18n.t('groups.request_membership_pm.body', { groupName });
this.sendAction("createNewMessageViaParams", names, title, body);
this.get('model').requestMembership().then(result => {
DiscourseURL.routeTo(result.relative_url);
}).catch(popupAjaxError).finally(() => {
this.set('loading', false);
});
} else {
this._showLoginModal();

View File

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

View File

@ -1,8 +1,8 @@
import { keyDirty } from 'discourse/widgets/widget';
import { diff, patch } from 'virtual-dom';
import { WidgetClickHook } from 'discourse/widgets/hooks';
import { renderedKey, queryRegistry } from 'discourse/widgets/widget';
import { queryRegistry } from 'discourse/widgets/widget';
import { getRegister } from 'discourse-common/lib/get-owner';
import DirtyKeys from 'discourse/lib/dirty-keys';
const _cleanCallbacks = {};
export function addWidgetCleanCallback(widgetName, fn) {
@ -18,6 +18,7 @@ export default Ember.Component.extend({
_renderCallback: null,
_childEvents: null,
_dispatched: null,
dirtyKeys: null,
init() {
this._super();
@ -34,6 +35,7 @@ export default Ember.Component.extend({
this._childEvents = [];
this._connected = [];
this._dispatched = [];
this.dirtyKeys = new DirtyKeys(name);
},
didInsertElement() {
@ -73,7 +75,7 @@ export default Ember.Component.extend({
eventDispatched(eventName, key, refreshArg) {
const onRefresh = Ember.String.camelize(eventName.replace(/:/, '-'));
keyDirty(key, { onRefresh, refreshArg });
this.dirtyKeys.keyDirty(key, { onRefresh, refreshArg });
this.queueRerender();
},
@ -104,7 +106,10 @@ export default Ember.Component.extend({
const t0 = new Date().getTime();
const args = this.get('args') || this.buildArgs();
const opts = { model: this.get('model') };
const opts = {
model: this.get('model'),
dirtyKeys: this.dirtyKeys,
};
const newTree = new this._widgetClass(args, this.register, opts);
newTree._rerenderable = this;
@ -122,8 +127,8 @@ export default Ember.Component.extend({
this._renderCallback = null;
}
this.afterRender();
this.dirtyKeys.renderedKey('*');
Ember.run.scheduleOnce('afterRender', () => renderedKey('*'));
if (this.profileWidget) {
console.log(new Date().getTime() - t0);
}

View File

@ -1,5 +1,4 @@
import DiscourseURL from 'discourse/lib/url';
import { keyDirty } from 'discourse/widgets/widget';
import MountWidget from 'discourse/components/mount-widget';
import { cloak, uncloak } from 'discourse/widgets/post-stream';
import { isWorkaroundActive } from 'discourse/lib/safari-hacks';
@ -245,13 +244,13 @@ export default MountWidget.extend({
this.appEvents.on('post-stream:refresh', args => {
if (args) {
if (args.id) {
keyDirty(`post-${args.id}`);
this.dirtyKeys.keyDirty(`post-${args.id}`);
if (args.refreshLikes) {
keyDirty(`post-menu-${args.id}`, { onRefresh: 'refreshLikes' });
this.dirtyKeys.keyDirty(`post-menu-${args.id}`, { onRefresh: 'refreshLikes' });
}
} else if (args.force) {
keyDirty(`*`);
this.dirtyKeys.forceAll();
}
}
this.queueRerender();

View File

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

View File

@ -2,13 +2,7 @@ import { propertyEqual } from 'discourse/lib/computed';
import { actionDescription } from "discourse/components/small-action";
export default Ember.Component.extend({
classNameBindings: [":item", "item.hidden", "item.deleted", "moderatorAction"],
classNameBindings: [":item", "item.hidden", "item.deleted:deleted", "moderatorAction"],
moderatorAction: propertyEqual("item.post_type", "site.post_types.moderator_action"),
actionDescription: actionDescription("item.action_code", "item.created_at", "item.username"),
actions: {
removeBookmark(userAction) {
this.sendAction("removeBookmark", userAction);
}
}
});

View File

@ -1,10 +1,11 @@
import MountWidget from 'discourse/components/mount-widget';
export default MountWidget.extend({
classNames: 'topic-admin-menu-button-container',
tagName: 'span',
widget: "topic-admin-menu-button",
buildArgs() {
return this.getProperties('topic', 'fixed', 'openUpwards');
return this.getProperties('topic', 'fixed', 'openUpwards', 'rightSide');
}
});

View File

@ -92,17 +92,18 @@ export default Ember.Component.extend(CleansUp, {
this.appEvents.off('topic-entrance:show');
},
_jumpTo(destination) {
this.cleanUp();
DiscourseURL.routeTo(destination);
},
actions: {
enterTop() {
const topic = this.get('topic');
this.appEvents.trigger('header:update-topic', topic);
DiscourseURL.routeTo(topic.get('url'));
this._jumpTo(this.get('topic.url'));
},
enterBottom() {
const topic = this.get('topic');
this.appEvents.trigger('header:update-topic', topic);
DiscourseURL.routeTo(topic.get('lastPostUrl'));
this._jumpTo(this.get('topic.lastPostUrl'));
}
}
});

View File

@ -20,7 +20,7 @@ export default Ember.Component.extend({
@computed('postStream.loaded', 'topic.currentPost', 'postStream.filteredPostsCount')
hideProgress(loaded, currentPost, filteredPostsCount) {
return (!loaded) || (!currentPost) || (filteredPostsCount < 2);
return (!loaded) || (!currentPost) || (!this.site.mobileView && filteredPostsCount < 2);
},
@computed('postStream.filteredPostsCount')
@ -52,8 +52,14 @@ export default Ember.Component.extend({
},
_topicScrolled(event) {
this.set('progressPosition', event.postIndex);
this._streamPercentage = event.percent;
if (this.get('docked')) {
this.set('progressPosition', this.get('postStream.filteredPostsCount'));
this._streamPercentage = 1.0;
} else {
this.set('progressPosition', event.postIndex);
this._streamPercentage = event.percent;
}
this._updateBar();
},
@ -110,11 +116,10 @@ export default Ember.Component.extend({
},
_dock() {
const maximumOffset = $('#topic-footer-buttons').offset(),
const maximumOffset = $('#topic-bottom').offset(),
composerHeight = $('#reply-control').height() || 0,
$topicProgressWrapper = this.$(),
offset = window.pageYOffset || $('html').scrollTop(),
topicProgressHeight = $('#topic-progress').height();
offset = window.pageYOffset || $('html').scrollTop();
if (!$topicProgressWrapper || $topicProgressWrapper.length === 0) {
return;
@ -124,7 +129,13 @@ export default Ember.Component.extend({
if (maximumOffset) {
const threshold = maximumOffset.top;
const windowHeight = $(window).height();
isDocked = offset >= threshold - windowHeight + topicProgressHeight + composerHeight;
const headerHeight = $('header').outerHeight(true);
if (this.capabilities.isIOS) {
isDocked = offset >= (threshold - windowHeight - headerHeight + composerHeight);
} else {
isDocked = offset >= (threshold - windowHeight + composerHeight);
}
}
const dockPos = $(document).height() - $('#topic-bottom').offset().top;

View File

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

View File

@ -1,6 +1,7 @@
import LoadMore from "discourse/mixins/load-more";
import ClickTrack from 'discourse/lib/click-track';
import { selectedText } from 'discourse/lib/utilities';
import Post from 'discourse/models/post';
export default Ember.Component.extend(LoadMore, {
loading: false,
@ -44,6 +45,13 @@ export default Ember.Component.extend(LoadMore, {
}.on('willDestroyElement'),
actions: {
removeBookmark(userAction) {
const stream = this.get('stream');
Post.updateBookmark(userAction.get("post_id"), false).then(() => {
stream.remove(userAction);
});
},
loadMore() {
if (this.get('loading')) { return; }

View File

@ -5,6 +5,8 @@ import { extractError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend(ModalFunctionality, {
offerHelp: null,
helpSeen: false,
@computed('accountEmailOrUsername', 'disabled')
submitDisabled(accountEmailOrUsername, disabled) {
@ -35,8 +37,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (data.user_found === true) {
key += '_found';
this.set('accountEmailOrUsername', '');
bootbox.alert(I18n.t(key, {email: escaped, username: escaped}));
this.send("closeModal");
this.set('offerHelp', I18n.t(key, {email: escaped, username: escaped}));
} else {
if (data.user_found === false) {
key += '_not_found';
@ -52,6 +53,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
});
return false;
},
ok() {
this.send('closeModal');
},
help() {
this.setProperties({ offerHelp: I18n.t('forgot_password.help'), helpSeen: true });
}
}

View File

@ -46,6 +46,14 @@ export default Ember.Controller.extend({
return Em.isEmpty(q);
},
@computed('q')
highlightQuery(q) {
if (!q) { return; }
// remove l which can be used for sorting
return _.reject(q.split(/\s+/), t => t === 'l').join(' ');
},
@computed('skip_context', 'context')
searchContextEnabled: {
get(skip,context){

View File

@ -4,6 +4,9 @@ const _buttons = [];
const alwaysTrue = () => true;
function identity() {
}
function addBulkButton(action, key, opts) {
opts = opts || {};
@ -72,7 +75,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
this.perform(operation).then(topics => {
if (topics) {
topics.forEach(cb);
(this.get('refreshClosure') || Ember.k)();
(this.get('refreshClosure') || identity)();
this.send('closeModal');
}
});
@ -80,7 +83,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
performAndRefresh(operation) {
return this.perform(operation).then(() => {
(this.get('refreshClosure') || Ember.k)();
(this.get('refreshClosure') || identity)();
this.send('closeModal');
});
},
@ -145,7 +148,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
this.perform({type: 'change_category', category_id: categoryId}).then(topics => {
topics.forEach(t => t.set('category', category));
(this.get('refreshClosure') || Ember.k)();
(this.get('refreshClosure') || identity)();
this.send('closeModal');
});
},

View File

@ -196,7 +196,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
const quotedText = Quote.build(post, buffer);
composerOpts.quote = quotedText;
if (composer.get('model.viewOpen')) {
this.appEvents.trigger('composer:insert-text', quotedText);
this.appEvents.trigger('composer:insert-block', quotedText);
} else if (composer.get('model.viewDraft')) {
const model = composer.get('model');
model.set('reply', model.get('reply') + quotedText);
@ -320,7 +320,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
composerController.get('content.action') === Composer.REPLY) {
composerController.set('content.post', post);
composerController.set('content.composeState', Composer.OPEN);
this.appEvents.trigger('composer:insert-text', quotedText.trim());
this.appEvents.trigger('composer:insert-block', quotedText.trim());
} else {
const opts = {

View File

@ -12,6 +12,7 @@ export default Ember.Controller.extend({
canLoadMore: true,
invitesLoading: false,
reinvitedAll: false,
rescindedAll: false,
init: function() {
this._super();
@ -32,7 +33,7 @@ export default Ember.Controller.extend({
inviteRedeemed: Em.computed.equal('filter', 'redeemed'),
showReinviteAllButton: function() {
showBulkActionButtons: function() {
return (this.get('filter') === "pending" && this.get('model').invites.length > 4 && this.currentUser.get('staff'));
}.property('filter'),
@ -86,17 +87,27 @@ export default Ember.Controller.extend({
return false;
},
rescindAll() {
bootbox.confirm(I18n.t("user.invited.rescind_all_confirm"), confirm => {
if (confirm) {
Invite.rescindAll().then(() => {
this.set('rescindedAll', true);
this.get('model.invites').clear();
}).catch(popupAjaxError);
}
});
},
reinvite(invite) {
invite.reinvite();
return false;
},
reinviteAll() {
const self = this;
bootbox.confirm(I18n.t("user.invited.reinvite_all_confirm"), confirm => {
if (confirm) {
Invite.reinviteAll().then(function() {
self.set('reinvitedAll', true);
Invite.reinviteAll().then(() => {
this.set('reinvitedAll', true);
}).catch(popupAjaxError);
}
});

View File

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

View File

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

View File

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

View File

@ -358,10 +358,22 @@ export default function(options) {
$(this).on('keyup.autocomplete', function(e) {
if ([keys.esc, keys.enter].indexOf(e.which) !== -1) return true;
var cp = caretPosition(me[0]);
let cp = caretPosition(me[0]);
const key = me[0].value[cp-1];
if (options.key && completeStart === null && cp > 0) {
var key = me[0].value[cp-1];
if (options.key) {
if (options.onKeyUp && key !== options.key) {
let match = options.onKeyUp(me.val(), cp);
if (match) {
completeStart = cp - match[0].length;
completeEnd = completeStart + match[0].length - 1;
let term = match[0].substring(1, match[0].length);
updateAutoComplete(dataSource(term, options));
}
}
}
if (completeStart === null && cp > 0) {
if (key === options.key) {
var prevChar = me.val().charAt(cp-2);
if (checkTriggerRule() && (!prevChar || allowedLettersRegex.test(prevChar))) {
@ -370,7 +382,7 @@ export default function(options) {
}
}
} else if (completeStart !== null) {
var term = me.val().substring(completeStart + (options.key ? 1 : 0), cp);
let term = me.val().substring(completeStart + (options.key ? 1 : 0), cp);
updateAutoComplete(dataSource(term, options));
}
});

View File

@ -0,0 +1,32 @@
export default class DirtyKeys {
constructor(name) {
this.name = name;
this._keys = {};
}
keyDirty(key, options) {
options = options || {};
options.dirty = true;
this._keys[key] = options;
}
forceAll() {
this.keyDirty('*');
}
allDirty() {
return !!this._keys['*'];
}
optionsFor(key) {
return this._keys[key] || { dirty: false };
}
renderedKey(key) {
if (key === '*') {
this._keys = {};
} else {
delete this._keys[key];
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -22,7 +22,7 @@ import { attachAdditionalPanel } from 'discourse/widgets/header';
// If you add any methods to the API ensure you bump up this number
const PLUGIN_API_VERSION = '0.8.6';
const PLUGIN_API_VERSION = '0.8.7';
class PluginApi {
constructor(version, container) {
@ -39,6 +39,25 @@ class PluginApi {
return this.container.lookup('current-user:main');
}
/**
* Allows you to overwrite or extend methods in a class.
*
* For example:
*
* ```
* api.modifyClass('controller:composer', {
* actions: {
* newActionHere() { }
* }
* });
* ```
**/
modifyClass(resolverName, changes) {
const klass = this.container.factoryFor(resolverName);
klass.class.reopen(changes);
return klass;
}
/**
* Used for decorating the `cooked` content of a post after it is rendered using
* jQuery.
@ -61,7 +80,7 @@ class PluginApi {
if (!opts.onlyStream) {
decorate(ComposerEditor, 'previewRefreshed', callback);
decorate(this.container.lookupFactory('component:user-stream'), 'didInsertElement', callback);
decorate(this.container.factoryFor('component:user-stream').class, 'didInsertElement', callback);
}
}
@ -170,7 +189,7 @@ class PluginApi {
* ```
**/
attachWidgetAction(widget, actionName, fn) {
const widgetClass = this.container.lookupFactory(`widget:${widget}`);
const widgetClass = this.container.factoryFor(`widget:${widget}`).class;
widgetClass.prototype[actionName] = fn;
}

View File

@ -56,7 +56,7 @@ export default Ember.Object.extend(Ember.Array, {
},
finishedPrepending(postIds) {
this._changeArray(Ember.K, 0, 0, postIds.length);
this._changeArray(function() { }, 0, 0, postIds.length);
},
objectAt(index) {

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
// A mixin where hitting ESC calls `cancelled` and ctrl+enter calls `save.
export default {
keyDown(e) {
if (e.which === 27) {
this.sendAction('cancelled');
return false;
} else if (e.which === 13 && (e.ctrlKey || e.metaKey)) {
// CTRL+ENTER or CMD+ENTER
this.sendAction('save');
return false;
}
},
};

View File

@ -31,7 +31,7 @@ const Scrolling = Ember.Mixin.create({
opts = opts || { debounce: 100 };
// So we can not call the scrolled event while transitioning
const router = Discourse.__container__.lookup('router:main').router;
const router = Discourse.__container__.lookup('router:main')._routerMicrolib;
let onScrollMethod = () => {
if (router.activeTransition) { return; }

View File

@ -1,26 +0,0 @@
import Post from 'discourse/models/post';
export default Post.extend({
_attachCategory: function () {
const categoryId = this.get("category_id");
if (categoryId) {
this.set("category", Discourse.Category.findById(categoryId));
}
}.on("init"),
presentName: Ember.computed.or('name', 'username'),
sameUser: function() {
return this.get("username") === Discourse.User.currentProp("username");
}.property("username"),
descriptionKey: function () {
if (this.get("reply_to_post_number")) {
return this.get("sameUser") ? "you_replied_to_post" : "user_replied_to_post";
} else {
return this.get("sameUser") ? "you_replied_to_topic" : "user_replied_to_topic";
}
}.property("reply_to_post_number", "sameUser")
});

View File

@ -2,7 +2,6 @@ import { ajax } from 'discourse/lib/ajax';
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
import GroupHistory from 'discourse/models/group-history';
import RestModel from 'discourse/models/rest';
import { popupAjaxError } from 'discourse/lib/ajax-error';
const Group = RestModel.extend({
limit: 50,
@ -114,23 +113,27 @@ const Group = RestModel.extend({
return aliasLevel === '99';
},
@observes("visible", "canEveryoneMention")
@observes("visibility_level", "canEveryoneMention")
_updateAllowMembershipRequests() {
if (!this.get('visible') || !this.get('canEveryoneMention')) {
if (this.get('visibility_level') !== 0 || !this.get('canEveryoneMention')) {
this.set ('allow_membership_requests', false);
}
},
@observes("visible")
@observes("visibility_level")
_updatePublic() {
if (!this.get('visible')) this.set('public', false);
let visibility_level = parseInt(this.get('visibility_level'));
if (visibility_level !== 0) {
this.set('public', false);
this.set('allow_membership_requests', false);
}
},
asJSON() {
return {
name: this.get('name'),
alias_level: this.get('alias_level'),
visible: !!this.get('visible'),
visibility_level: this.get('visibility_level'),
automatic_membership_email_domains: this.get('emailDomains'),
automatic_membership_retroactive: !!this.get('automatic_membership_retroactive'),
title: this.get('title'),
@ -202,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({

View File

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

View File

@ -3,7 +3,7 @@ const RestModel = Ember.Object.extend({
isCreated: Ember.computed.equal('__state', 'created'),
isSaving: false,
afterUpdate: Ember.K,
afterUpdate() { },
update(props) {
if (this.get('isSaving')) { return Ember.RSVP.reject(); }

View File

@ -297,7 +297,16 @@ export default Ember.Object.extend({
if (existing) {
delete obj.id;
const klass = this.register.lookupFactory('model:' + type) || RestModel;
let klass = this.register.lookupFactory('model:' + type);
if (klass && klass.class) {
klass = klass.class;
}
if (!klass) {
klass = RestModel;
}
existing.setProperties(klass.munge(obj));
obj.id = id;
return existing;

View File

@ -1,6 +1,6 @@
import { ajax } from 'discourse/lib/ajax';
import { url } from 'discourse/lib/computed';
import AdminPost from 'discourse/models/admin-post';
import UserAction from 'discourse/models/user-action';
export default Discourse.Model.extend({
loaded: false,
@ -36,7 +36,7 @@ export default Discourse.Model.extend({
return ajax(this.get("url"), { cache: false }).then(function (result) {
if (result) {
const posts = result.map(function (post) { return AdminPost.create(post); });
const posts = result.map(function (post) { return UserAction.create(post); });
self.get("content").pushObjects(posts);
self.setProperties({
loaded: true,

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
{{cooked}}

View File

@ -2,5 +2,8 @@
{{fa-icon icon}}
{{/if}}
{{{translatedLabel}}}
{{#if translatedLabel}}
<span class='d-button-label'>{{{translatedLabel}}}</span>
{{/if}}
{{yield}}

View File

@ -30,5 +30,6 @@
<div class="d-editor-preview">
{{{preview}}}
</div>
{{plugin-outlet name="editor-preview"}}
</div>
</div>

View File

@ -1 +1 @@
{{input type="text" class="date-picker" placeholder=placeholder}}
{{input type="text" class="date-picker" placeholder=placeholder value=value}}

View File

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@
{{#if editing}}
{{d-editor value=buffered.raw}}
{{else}}
{{{cook-text post.raw}}}
{{cook-text post.raw}}
{{/if}}
</div>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
<div class="container">
<div class="title-wrapper">
{{yield}}
</div>
{{plugin-outlet name="topic-title" args=(hash model=model)}}
</div>

View File

@ -0,0 +1,3 @@
{{#each stream.content as |item|}}
{{stream-item item=item removeBookmark=(action "removeBookmark")}}
{{/each}}

View File

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

View File

@ -107,7 +107,7 @@
</span>
{{#if result.blurb}}
{{#highlight-text highlight=q}}
{{#highlight-text highlight=highlightQuery}}
{{{unbound result.blurb}}}
{{/highlight-text}}
{{/if}}

View File

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

View File

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

View File

@ -45,7 +45,6 @@
<td>
{{#group-membership-button model=group
createNewMessageViaParams='createNewMessageViaParams'
showMembershipStatus=true
groupUserIds=groups.extras.group_user_ids
showLogin='showLogin'}}

View File

@ -41,7 +41,7 @@
{{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn}}
&nbsp;{{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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

@ -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 || "&#9632;";
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('&#9632;');
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);
}

View File

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

View File

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

View File

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