commit
bd509f5550
|
@ -106,3 +106,4 @@ config/version.rb
|
||||||
bundler_stubs/*
|
bundler_stubs/*
|
||||||
|
|
||||||
vendor/bundle/*
|
vendor/bundle/*
|
||||||
|
*.db
|
||||||
|
|
|
@ -45,6 +45,7 @@ before_install:
|
||||||
- eslint app/assets/javascripts
|
- eslint app/assets/javascripts
|
||||||
- eslint --ext .es6 app/assets/javascripts
|
- eslint --ext .es6 app/assets/javascripts
|
||||||
- eslint --ext .es6 test/javascripts
|
- eslint --ext .es6 test/javascripts
|
||||||
|
- eslint --ext .es6 plugins/**/assets/javascripts
|
||||||
- eslint test/javascripts
|
- eslint test/javascripts
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
|
|
6
Gemfile
6
Gemfile
|
@ -61,7 +61,7 @@ gem 'fast_xs'
|
||||||
gem 'fast_xor'
|
gem 'fast_xor'
|
||||||
|
|
||||||
# while we sort out https://github.com/sdsykes/fastimage/pull/46
|
# while we sort out https://github.com/sdsykes/fastimage/pull/46
|
||||||
gem 'discourse_fastimage', require: 'fastimage'
|
gem 'discourse_fastimage', '2.0.2', require: 'fastimage'
|
||||||
gem 'aws-sdk', require: false
|
gem 'aws-sdk', require: false
|
||||||
gem 'excon', require: false
|
gem 'excon', require: false
|
||||||
gem 'unf', require: false
|
gem 'unf', require: false
|
||||||
|
@ -74,7 +74,7 @@ gem 'email_reply_trimmer', '0.1.3'
|
||||||
gem 'image_optim', '0.20.2'
|
gem 'image_optim', '0.20.2'
|
||||||
gem 'multi_json'
|
gem 'multi_json'
|
||||||
gem 'mustache'
|
gem 'mustache'
|
||||||
gem 'nokogiri', '1.6.8.rc3'
|
gem 'nokogiri'
|
||||||
gem 'omniauth'
|
gem 'omniauth'
|
||||||
gem 'omniauth-openid'
|
gem 'omniauth-openid'
|
||||||
gem 'openid-redis-store'
|
gem 'openid-redis-store'
|
||||||
|
@ -125,7 +125,7 @@ group :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test, :development do
|
group :test, :development do
|
||||||
gem 'rspec', '~> 3.2.0'
|
gem 'rspec'
|
||||||
gem 'mock_redis'
|
gem 'mock_redis'
|
||||||
gem 'listen', '0.7.3', require: false
|
gem 'listen', '0.7.3', require: false
|
||||||
gem 'certified', require: false
|
gem 'certified', require: false
|
||||||
|
|
44
Gemfile.lock
44
Gemfile.lock
|
@ -73,7 +73,7 @@ GEM
|
||||||
diff-lcs (1.2.5)
|
diff-lcs (1.2.5)
|
||||||
discourse-qunit-rails (0.0.9)
|
discourse-qunit-rails (0.0.9)
|
||||||
railties
|
railties
|
||||||
discourse_fastimage (2.0.0)
|
discourse_fastimage (2.0.2)
|
||||||
docile (1.1.5)
|
docile (1.1.5)
|
||||||
domain_name (0.5.25)
|
domain_name (0.5.25)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
|
@ -174,7 +174,7 @@ GEM
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
mustache (1.0.3)
|
mustache (1.0.3)
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
nokogiri (1.6.8.rc3)
|
nokogiri (1.6.8)
|
||||||
mini_portile2 (~> 2.1.0)
|
mini_portile2 (~> 2.1.0)
|
||||||
pkg-config (~> 1.1.7)
|
pkg-config (~> 1.1.7)
|
||||||
nokogumbo (1.4.7)
|
nokogumbo (1.4.7)
|
||||||
|
@ -294,33 +294,33 @@ GEM
|
||||||
netrc (~> 0.7)
|
netrc (~> 0.7)
|
||||||
rinku (2.0.0)
|
rinku (2.0.0)
|
||||||
rmmseg-cpp (0.2.9)
|
rmmseg-cpp (0.2.9)
|
||||||
rspec (3.2.0)
|
rspec (3.4.0)
|
||||||
rspec-core (~> 3.2.0)
|
rspec-core (~> 3.4.0)
|
||||||
rspec-expectations (~> 3.2.0)
|
rspec-expectations (~> 3.4.0)
|
||||||
rspec-mocks (~> 3.2.0)
|
rspec-mocks (~> 3.4.0)
|
||||||
rspec-core (3.2.3)
|
rspec-core (3.4.4)
|
||||||
rspec-support (~> 3.2.0)
|
rspec-support (~> 3.4.0)
|
||||||
rspec-expectations (3.2.1)
|
rspec-expectations (3.4.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.2.0)
|
rspec-support (~> 3.4.0)
|
||||||
rspec-given (3.7.1)
|
rspec-given (3.7.1)
|
||||||
given_core (= 3.7.1)
|
given_core (= 3.7.1)
|
||||||
rspec (>= 2.14.0)
|
rspec (>= 2.14.0)
|
||||||
rspec-html-matchers (0.7.0)
|
rspec-html-matchers (0.7.0)
|
||||||
nokogiri (~> 1)
|
nokogiri (~> 1)
|
||||||
rspec (~> 3)
|
rspec (~> 3)
|
||||||
rspec-mocks (3.2.1)
|
rspec-mocks (3.4.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.2.0)
|
rspec-support (~> 3.4.0)
|
||||||
rspec-rails (3.2.3)
|
rspec-rails (3.4.2)
|
||||||
actionpack (>= 3.0, < 4.3)
|
actionpack (>= 3.0, < 4.3)
|
||||||
activesupport (>= 3.0, < 4.3)
|
activesupport (>= 3.0, < 4.3)
|
||||||
railties (>= 3.0, < 4.3)
|
railties (>= 3.0, < 4.3)
|
||||||
rspec-core (~> 3.2.0)
|
rspec-core (~> 3.4.0)
|
||||||
rspec-expectations (~> 3.2.0)
|
rspec-expectations (~> 3.4.0)
|
||||||
rspec-mocks (~> 3.2.0)
|
rspec-mocks (~> 3.4.0)
|
||||||
rspec-support (~> 3.2.0)
|
rspec-support (~> 3.4.0)
|
||||||
rspec-support (3.2.2)
|
rspec-support (3.4.1)
|
||||||
rtlit (0.0.5)
|
rtlit (0.0.5)
|
||||||
ruby-openid (2.7.0)
|
ruby-openid (2.7.0)
|
||||||
ruby-readability (0.7.0)
|
ruby-readability (0.7.0)
|
||||||
|
@ -410,7 +410,7 @@ DEPENDENCIES
|
||||||
byebug
|
byebug
|
||||||
certified
|
certified
|
||||||
discourse-qunit-rails
|
discourse-qunit-rails
|
||||||
discourse_fastimage
|
discourse_fastimage (= 2.0.2)
|
||||||
email_reply_trimmer (= 0.1.3)
|
email_reply_trimmer (= 0.1.3)
|
||||||
ember-rails (= 0.18.5)
|
ember-rails (= 0.18.5)
|
||||||
ember-source (= 1.12.2)
|
ember-source (= 1.12.2)
|
||||||
|
@ -444,7 +444,7 @@ DEPENDENCIES
|
||||||
mock_redis
|
mock_redis
|
||||||
multi_json
|
multi_json
|
||||||
mustache
|
mustache
|
||||||
nokogiri (= 1.6.8.rc3)
|
nokogiri
|
||||||
oj
|
oj
|
||||||
omniauth
|
omniauth
|
||||||
omniauth-facebook
|
omniauth-facebook
|
||||||
|
@ -475,7 +475,7 @@ DEPENDENCIES
|
||||||
rest-client
|
rest-client
|
||||||
rinku
|
rinku
|
||||||
rmmseg-cpp
|
rmmseg-cpp
|
||||||
rspec (~> 3.2.0)
|
rspec
|
||||||
rspec-given
|
rspec-given
|
||||||
rspec-html-matchers
|
rspec-html-matchers
|
||||||
rspec-rails
|
rspec-rails
|
||||||
|
@ -500,4 +500,4 @@ DEPENDENCIES
|
||||||
unicorn
|
unicorn
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.12.3
|
1.12.5
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
# See https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md
|
# See https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md
|
||||||
#
|
#
|
||||||
Vagrant.configure("2") do |config|
|
Vagrant.configure("2") do |config|
|
||||||
config.vm.box = 'discourse/discourse-1.3.0'
|
config.vm.box = 'discourse-16.04'
|
||||||
config.vm.box_url = "http://discourse-vms.s3.amazonaws.com/discourse-1.3.0.box"
|
config.vm.box_url = "https://www.dropbox.com/s/2132770g1e05c6d/discourse.box?dl=1"
|
||||||
|
|
||||||
# Make this VM reachable on the host network as well, so that other
|
# Make this VM reachable on the host network as well, so that other
|
||||||
# VM's running other browsers can access our dev server.
|
# VM's running other browsers can access our dev server.
|
||||||
|
@ -43,6 +43,6 @@ Vagrant.configure("2") do |config|
|
||||||
config.vm.network :forwarded_port, guest: 1080, host: 4080 # Mailcatcher
|
config.vm.network :forwarded_port, guest: 1080, host: 4080 # Mailcatcher
|
||||||
|
|
||||||
nfs_setting = RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /linux/
|
nfs_setting = RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /linux/
|
||||||
config.vm.synced_folder ".", "/vagrant", id: "vagrant-root", :nfs => nfs_setting
|
config.vm.synced_folder ".", "/vagrant", id: "vagrant-root"
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -65,7 +65,11 @@ export default Ember.Component.extend({
|
||||||
|
|
||||||
const $elem = this.$();
|
const $elem = this.$();
|
||||||
const minimumResultsForSearch = this.capabilities.isIOS ? -1 : 5;
|
const minimumResultsForSearch = this.capabilities.isIOS ? -1 : 5;
|
||||||
$elem.select2({formatResult: this.comboTemplate, minimumResultsForSearch, width: 'resolve'});
|
$elem.select2({
|
||||||
|
formatResult: this.comboTemplate, minimumResultsForSearch,
|
||||||
|
width: 'resolve',
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
|
||||||
const castInteger = this.get('castInteger');
|
const castInteger = this.get('castInteger');
|
||||||
$elem.on("change", e => {
|
$elem.on("change", e => {
|
||||||
|
|
|
@ -82,7 +82,7 @@ export default Ember.Component.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
this._bindUploadTarget();
|
this._bindUploadTarget();
|
||||||
this.appEvents.trigger('composer:opened');
|
this.appEvents.trigger('composer:will-open');
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed('composer.reply', 'composer.replyLength', 'composer.missingReplyCharacters', 'composer.minimumPostLength', 'lastValidatedAt')
|
@computed('composer.reply', 'composer.replyLength', 'composer.missingReplyCharacters', 'composer.minimumPostLength', 'lastValidatedAt')
|
||||||
|
@ -341,6 +341,7 @@ export default Ember.Component.extend({
|
||||||
|
|
||||||
@on('willDestroyElement')
|
@on('willDestroyElement')
|
||||||
_composerClosed() {
|
_composerClosed() {
|
||||||
|
this.appEvents.trigger('composer:will-close');
|
||||||
Ember.run.next(() => {
|
Ember.run.next(() => {
|
||||||
$('#main-outlet').css('padding-bottom', 0);
|
$('#main-outlet').css('padding-bottom', 0);
|
||||||
// need to wait a bit for the "slide down" transition of the composer
|
// need to wait a bit for the "slide down" transition of the composer
|
||||||
|
@ -361,7 +362,7 @@ export default Ember.Component.extend({
|
||||||
this._resetUpload(true);
|
this._resetUpload(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
showOptions() {
|
showOptions(toolbarEvent) {
|
||||||
// long term we want some smart positioning algorithm in popup-menu
|
// long term we want some smart positioning algorithm in popup-menu
|
||||||
// the problem is that positioning in a fixed panel is a nightmare
|
// the problem is that positioning in a fixed panel is a nightmare
|
||||||
// cause offsetParent can end up returning a fixed element and then
|
// cause offsetParent can end up returning a fixed element and then
|
||||||
|
@ -387,9 +388,8 @@ export default Ember.Component.extend({
|
||||||
left = replyWidth - popupWidth - 40;
|
left = replyWidth - popupWidth - 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendAction('showOptions', { position: "absolute",
|
this.sendAction('showOptions', toolbarEvent,
|
||||||
left: left,
|
{ position: "absolute", left, top });
|
||||||
top: top });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showUploadModal(toolbarEvent) {
|
showUploadModal(toolbarEvent) {
|
||||||
|
@ -419,7 +419,7 @@ export default Ember.Component.extend({
|
||||||
sendAction: 'showUploadModal'
|
sendAction: 'showUploadModal'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.get('canWhisper')) {
|
if (this.get("popupMenuOptions").some(option => option.condition)) {
|
||||||
toolbar.addButton({
|
toolbar.addButton({
|
||||||
id: 'options',
|
id: 'options',
|
||||||
group: 'extras',
|
group: 'extras',
|
||||||
|
@ -458,6 +458,7 @@ export default Ember.Component.extend({
|
||||||
// Paint oneboxes
|
// Paint oneboxes
|
||||||
$('a.onebox', $preview).each((i, e) => Discourse.Onebox.load(e, refresh));
|
$('a.onebox', $preview).each((i, e) => Discourse.Onebox.load(e, refresh));
|
||||||
this.trigger('previewRefreshed', $preview);
|
this.trigger('previewRefreshed', $preview);
|
||||||
|
this.sendAction('afterRefresh', $preview);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNameBindings: [':composer-popup', ':hidden', 'message.extraClass'],
|
||||||
|
|
||||||
|
@computed('message.templateName')
|
||||||
|
defaultLayout(templateName) {
|
||||||
|
return this.container.lookup(`template:composer/${templateName}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
this.$().show();
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
closeMessage() {
|
||||||
|
this.sendAction('closeMessage', this.get('message'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,162 @@
|
||||||
|
import LinkLookup from 'discourse/lib/link-lookup';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNameBindings: [':composer-popup-container', 'hidden'],
|
||||||
|
checkedMessages: false,
|
||||||
|
messages: null,
|
||||||
|
messagesByTemplate: null,
|
||||||
|
queuedForTyping: null,
|
||||||
|
_lastSimilaritySearch: null,
|
||||||
|
_similarTopicsMessage: null,
|
||||||
|
similarTopics: null,
|
||||||
|
|
||||||
|
hidden: Ember.computed.not('composer.viewOpen'),
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
this.reset();
|
||||||
|
this.appEvents.on('composer:typed-reply', this, this._typedReply);
|
||||||
|
this.appEvents.on('composer:opened', this, this._findMessages);
|
||||||
|
this.appEvents.on('composer:find-similar', this, this._findSimilar);
|
||||||
|
this.appEvents.on('composer-messages:close', this, this._closeTop);
|
||||||
|
this.appEvents.on('composer-messages:create', this, this._create);
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
this.appEvents.off('composer:typed-reply', this, this._typedReply);
|
||||||
|
this.appEvents.off('composer:opened', this, this._findMessages);
|
||||||
|
this.appEvents.off('composer:find-similar', this, this._findSimilar);
|
||||||
|
this.appEvents.off('composer-messages:close', this, this._closeTop);
|
||||||
|
this.appEvents.off('composer-messages:create', this, this._create);
|
||||||
|
},
|
||||||
|
|
||||||
|
_closeTop() {
|
||||||
|
const messages = this.get('messages');
|
||||||
|
messages.popObject();
|
||||||
|
this.set('messageCount', messages.get('length'));
|
||||||
|
},
|
||||||
|
|
||||||
|
_removeMessage(message) {
|
||||||
|
const messages = this.get('messages');
|
||||||
|
messages.removeObject(message);
|
||||||
|
this.set('messageCount', messages.get('length'));
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
closeMessage(message) {
|
||||||
|
this._removeMessage(message);
|
||||||
|
},
|
||||||
|
|
||||||
|
hideMessage(message) {
|
||||||
|
this._removeMessage(message);
|
||||||
|
// kind of hacky but the visibility depends on this
|
||||||
|
this.get('messagesByTemplate')[message.get('templateName')] = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
popup(message) {
|
||||||
|
const messagesByTemplate = this.get('messagesByTemplate');
|
||||||
|
const templateName = message.get('templateName');
|
||||||
|
|
||||||
|
if (!messagesByTemplate[templateName]) {
|
||||||
|
const messages = this.get('messages');
|
||||||
|
messages.pushObject(message);
|
||||||
|
this.set('messageCount', messages.get('length'));
|
||||||
|
messagesByTemplate[templateName] = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Resets all active messages.
|
||||||
|
// For example if composing a new post.
|
||||||
|
reset() {
|
||||||
|
if (this.isDestroying || this.isDestroyed) { return; }
|
||||||
|
this.setProperties({
|
||||||
|
messages: [],
|
||||||
|
messagesByTemplate: {},
|
||||||
|
queuedForTyping: [],
|
||||||
|
checkedMessages: false,
|
||||||
|
similarTopics: [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Called after the user has typed a reply.
|
||||||
|
// Some messages only get shown after being typed.
|
||||||
|
_typedReply() {
|
||||||
|
if (this.isDestroying || this.isDestroyed) { return; }
|
||||||
|
this.get('queuedForTyping').forEach(msg => this.send("popup", msg));
|
||||||
|
},
|
||||||
|
|
||||||
|
_create(info) {
|
||||||
|
this.reset();
|
||||||
|
this.send('popup', Ember.Object.create(info));
|
||||||
|
},
|
||||||
|
|
||||||
|
_findSimilar() {
|
||||||
|
const composer = this.get('composer');
|
||||||
|
|
||||||
|
// We don't care about similar topics unless creating a topic
|
||||||
|
if (!composer.get('creatingTopic')) { return; }
|
||||||
|
|
||||||
|
const origBody = composer.get('reply') || '';
|
||||||
|
const title = composer.get('title') || '';
|
||||||
|
|
||||||
|
// Ensure the fields are of the minimum length
|
||||||
|
if (origBody.length < Discourse.SiteSettings.min_body_similar_length) { return; }
|
||||||
|
if (title.length < Discourse.SiteSettings.min_title_similar_length) { return; }
|
||||||
|
|
||||||
|
// TODO pass the 200 in from somewhere
|
||||||
|
const body = origBody.substr(0, 200);
|
||||||
|
|
||||||
|
// Don't search over and over
|
||||||
|
const concat = title + body;
|
||||||
|
if (concat === this._lastSimilaritySearch) { return; }
|
||||||
|
this._lastSimilaritySearch = concat;
|
||||||
|
|
||||||
|
const similarTopics = this.get('similarTopics');
|
||||||
|
const message = this._similarTopicsMessage || composer.store.createRecord('composer-message', {
|
||||||
|
id: 'similar_topics',
|
||||||
|
templateName: 'similar-topics',
|
||||||
|
extraClass: 'similar-topics'
|
||||||
|
});
|
||||||
|
|
||||||
|
this._similarTopicsMessage = message;
|
||||||
|
|
||||||
|
composer.store.find('similar-topic', {title, raw: body}).then(newTopics => {
|
||||||
|
similarTopics.clear();
|
||||||
|
similarTopics.pushObjects(newTopics.get('content'));
|
||||||
|
|
||||||
|
if (similarTopics.get('length') > 0) {
|
||||||
|
message.set('similarTopics', similarTopics);
|
||||||
|
this.send('popup', message);
|
||||||
|
} else if (message) {
|
||||||
|
this.send('hideMessage', message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Figure out if there are any messages that should be displayed above the composer.
|
||||||
|
_findMessages() {
|
||||||
|
if (this.get('checkedMessages')) { return; }
|
||||||
|
|
||||||
|
const composer = this.get('composer');
|
||||||
|
const args = { composer_action: composer.get('action') };
|
||||||
|
const topicId = composer.get('topic.id');
|
||||||
|
const postId = composer.get('post.id');
|
||||||
|
|
||||||
|
if (topicId) { args.topic_id = topicId; }
|
||||||
|
if (postId) { args.post_id = postId; }
|
||||||
|
|
||||||
|
const queuedForTyping = this.get('queuedForTyping');
|
||||||
|
composer.store.find('composer-message', args).then(messages => {
|
||||||
|
|
||||||
|
// Checking composer messages on replies can give us a list of links to check for
|
||||||
|
// duplicates
|
||||||
|
if (messages.extras && messages.extras.duplicate_lookup) {
|
||||||
|
this.sendAction('addLinkLookup', new LinkLookup(messages.extras.duplicate_lookup));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('checkedMessages', true);
|
||||||
|
messages.forEach(msg => msg.wait_for_typing ? queuedForTyping.addObject(msg) : this.send('popup', msg));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -561,5 +561,4 @@ export default Ember.Component.extend({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { buildCategoryPanel } from 'discourse/components/edit-category-panel';
|
||||||
|
|
||||||
|
export default buildCategoryPanel('tags', {
|
||||||
|
});
|
|
@ -16,11 +16,14 @@ export default Ember.Component.extend({
|
||||||
_widgetClass: null,
|
_widgetClass: null,
|
||||||
_renderCallback: null,
|
_renderCallback: null,
|
||||||
_childEvents: null,
|
_childEvents: null,
|
||||||
|
_dispatched: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super();
|
this._super();
|
||||||
const name = this.get('widget');
|
const name = this.get('widget');
|
||||||
|
|
||||||
|
(this.get('delegated') || []).forEach(m => this.set(m, m));
|
||||||
|
|
||||||
this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`);
|
this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`);
|
||||||
|
|
||||||
if (!this._widgetClass) {
|
if (!this._widgetClass) {
|
||||||
|
@ -29,6 +32,7 @@ export default Ember.Component.extend({
|
||||||
|
|
||||||
this._childEvents = [];
|
this._childEvents = [];
|
||||||
this._connected = [];
|
this._connected = [];
|
||||||
|
this._dispatched = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
|
@ -50,7 +54,10 @@ export default Ember.Component.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
this._childEvents.forEach(evt => this.appEvents.off(evt));
|
this._dispatched.forEach(evt => {
|
||||||
|
const [eventName, caller] = evt;
|
||||||
|
this.appEvents.off(eventName, caller);
|
||||||
|
});
|
||||||
Ember.run.cancel(this._timeout);
|
Ember.run.cancel(this._timeout);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -71,9 +78,10 @@ export default Ember.Component.extend({
|
||||||
|
|
||||||
dispatch(eventName, key) {
|
dispatch(eventName, key) {
|
||||||
this._childEvents.push(eventName);
|
this._childEvents.push(eventName);
|
||||||
this.appEvents.on(eventName, refreshArg => {
|
|
||||||
this.eventDispatched(eventName, key, refreshArg);
|
const caller = refreshArg => this.eventDispatched(eventName, key, refreshArg);
|
||||||
});
|
this._dispatched.push([eventName, caller]);
|
||||||
|
this.appEvents.on(eventName, caller);
|
||||||
},
|
},
|
||||||
|
|
||||||
queueRerender(callback) {
|
queueRerender(callback) {
|
||||||
|
@ -93,7 +101,6 @@ export default Ember.Component.extend({
|
||||||
if (!this._widgetClass) { return; }
|
if (!this._widgetClass) { return; }
|
||||||
|
|
||||||
const t0 = new Date().getTime();
|
const t0 = new Date().getTime();
|
||||||
|
|
||||||
const args = this.get('args') || this.buildArgs();
|
const args = this.get('args') || this.buildArgs();
|
||||||
const opts = { model: this.get('model') };
|
const opts = { model: this.get('model') };
|
||||||
const newTree = new this._widgetClass(args, this.container, opts);
|
const newTree = new this._widgetClass(args, this.container, opts);
|
||||||
|
@ -117,8 +124,6 @@ export default Ember.Component.extend({
|
||||||
if (this.profileWidget) {
|
if (this.profileWidget) {
|
||||||
console.log(new Date().getTime() - t0);
|
console.log(new Date().getTime() - t0);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,73 +1,49 @@
|
||||||
import DropdownButton from 'discourse/components/dropdown-button';
|
import DropdownButton from 'discourse/components/dropdown-button';
|
||||||
import NotificationLevels from 'discourse/lib/notification-levels';
|
import { all, buttonDetails } from 'discourse/lib/notification-levels';
|
||||||
|
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
const NotificationsButton = DropdownButton.extend({
|
export default DropdownButton.extend({
|
||||||
classNames: ['notification-options'],
|
classNames: ['notification-options'],
|
||||||
title: '',
|
title: '',
|
||||||
buttonIncludesText: true,
|
buttonIncludesText: true,
|
||||||
activeItem: Em.computed.alias('notificationLevel'),
|
activeItem: Em.computed.alias('notificationLevel'),
|
||||||
i18nPrefix: '',
|
i18nPrefix: '',
|
||||||
i18nPostfix: '',
|
i18nPostfix: '',
|
||||||
watchingClasses: 'fa fa-exclamation-circle watching',
|
|
||||||
trackingClasses: 'fa fa-circle tracking',
|
|
||||||
mutedClasses: 'fa fa-times-circle muted',
|
|
||||||
regularClasses: 'fa fa-circle-o regular',
|
|
||||||
|
|
||||||
options: function() {
|
@computed
|
||||||
return [['WATCHING', 'watching', this.watchingClasses],
|
dropDownContent() {
|
||||||
['TRACKING', 'tracking', this.trackingClasses],
|
const prefix = this.get('i18nPrefix');
|
||||||
['REGULAR', 'regular', this.regularClasses],
|
const postfix = this.get('i18nPostfix');
|
||||||
['MUTED', 'muted', this.mutedClasses]];
|
|
||||||
}.property(),
|
|
||||||
|
|
||||||
dropDownContent: function() {
|
return all.map(l => {
|
||||||
const contents = [],
|
const start = `${prefix}.${l.key}${postfix}`;
|
||||||
prefix = this.get('i18nPrefix'),
|
return {
|
||||||
postfix = this.get('i18nPostfix');
|
id: l.id,
|
||||||
|
title: I18n.t(`${start}.title`),
|
||||||
_.each(this.get('options'), function(pair) {
|
description: I18n.t(`${start}.description`),
|
||||||
if (postfix === '_pm' && pair[1] === 'regular') { return; }
|
styleClasses: `${l.key} fa fa-${l.icon}`
|
||||||
contents.push({
|
};
|
||||||
id: NotificationLevels[pair[0]],
|
|
||||||
title: I18n.t(prefix + '.' + pair[1] + postfix + '.title'),
|
|
||||||
description: I18n.t(prefix + '.' + pair[1] + postfix + '.description'),
|
|
||||||
styleClasses: pair[2]
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
return contents;
|
@computed('notificationLevel')
|
||||||
}.property(),
|
text(notificationLevel) {
|
||||||
|
const details = buttonDetails(notificationLevel);
|
||||||
|
const { key } = details;
|
||||||
|
const icon = iconHTML(details.icon, { class: key });
|
||||||
|
|
||||||
text: function() {
|
if (this.get('buttonIncludesText')) {
|
||||||
const self = this,
|
const prefix = this.get('i18nPrefix');
|
||||||
prefix = this.get('i18nPrefix'),
|
const postfix = this.get('i18nPostfix');
|
||||||
postfix = this.get('i18nPostfix');
|
const text = I18n.t(`${prefix}.${key}${postfix}.title`);
|
||||||
|
return `${icon} ${text}<span class='caret'></span>`;
|
||||||
const key = (function() {
|
} else {
|
||||||
switch (this.get('notificationLevel')) {
|
return `${icon} <span class='caret'></span>`;
|
||||||
case NotificationLevels.WATCHING: return 'watching';
|
}
|
||||||
case NotificationLevels.TRACKING: return 'tracking';
|
},
|
||||||
case NotificationLevels.MUTED: return 'muted';
|
|
||||||
default: return 'regular';
|
|
||||||
}
|
|
||||||
}).call(this);
|
|
||||||
|
|
||||||
const icon = (function() {
|
|
||||||
switch (key) {
|
|
||||||
case 'watching': return '<i class="' + self.watchingClasses + '"></i> ';
|
|
||||||
case 'tracking': return '<i class="' + self.trackingClasses + '"></i> ';
|
|
||||||
case 'muted': return '<i class="' + self.mutedClasses + '"></i> ';
|
|
||||||
default: return '<i class="' + self.regularClasses + '"></i> ';
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return icon + ( this.get('buttonIncludesText') ? I18n.t(prefix + '.' + key + postfix + ".title") : '') + "<span class='caret'></span>";
|
|
||||||
}.property('notificationLevel'),
|
|
||||||
|
|
||||||
clicked(/* id */) {
|
clicked(/* id */) {
|
||||||
// sub-class needs to implement this
|
// sub-class needs to implement this
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default NotificationsButton;
|
|
||||||
export { NotificationLevels };
|
|
||||||
|
|
|
@ -3,11 +3,12 @@ import { keyDirty } from 'discourse/widgets/widget';
|
||||||
import MountWidget from 'discourse/components/mount-widget';
|
import MountWidget from 'discourse/components/mount-widget';
|
||||||
import { cloak, uncloak } from 'discourse/widgets/post-stream';
|
import { cloak, uncloak } from 'discourse/widgets/post-stream';
|
||||||
import { isWorkaroundActive } from 'discourse/lib/safari-hacks';
|
import { isWorkaroundActive } from 'discourse/lib/safari-hacks';
|
||||||
|
import offsetCalculator from 'discourse/lib/offset-calculator';
|
||||||
|
|
||||||
function findTopView($posts, viewportTop, min, max) {
|
function findTopView($posts, viewportTop, min, max) {
|
||||||
if (max < min) { return min; }
|
if (max < min) { return min; }
|
||||||
|
|
||||||
while(max>min){
|
while (max > min) {
|
||||||
const mid = Math.floor((min + max) / 2);
|
const mid = Math.floor((min + max) / 2);
|
||||||
const $post = $($posts[mid]);
|
const $post = $($posts[mid]);
|
||||||
const viewBottom = $post.position().top + $post.height();
|
const viewBottom = $post.position().top + $post.height();
|
||||||
|
@ -26,8 +27,11 @@ export default MountWidget.extend({
|
||||||
widget: 'post-stream',
|
widget: 'post-stream',
|
||||||
_topVisible: null,
|
_topVisible: null,
|
||||||
_bottomVisible: null,
|
_bottomVisible: null,
|
||||||
|
_currentPost: null,
|
||||||
|
_currentVisible: null,
|
||||||
|
_currentPercent: null,
|
||||||
|
|
||||||
args: Ember.computed(function() {
|
buildArgs() {
|
||||||
return this.getProperties('posts',
|
return this.getProperties('posts',
|
||||||
'canCreatePost',
|
'canCreatePost',
|
||||||
'multiSelect',
|
'multiSelect',
|
||||||
|
@ -35,7 +39,7 @@ export default MountWidget.extend({
|
||||||
'selectedQuery',
|
'selectedQuery',
|
||||||
'selectedPostsCount',
|
'selectedPostsCount',
|
||||||
'searchService');
|
'searchService');
|
||||||
}).volatile(),
|
},
|
||||||
|
|
||||||
beforePatch() {
|
beforePatch() {
|
||||||
const $body = $(document);
|
const $body = $(document);
|
||||||
|
@ -66,7 +70,7 @@ export default MountWidget.extend({
|
||||||
const onscreen = [];
|
const onscreen = [];
|
||||||
const nearby = [];
|
const nearby = [];
|
||||||
|
|
||||||
let windowTop = $w.scrollTop();
|
const windowTop = $w.scrollTop();
|
||||||
|
|
||||||
const $posts = this.$('.onscreen-post, .cloaked-post');
|
const $posts = this.$('.onscreen-post, .cloaked-post');
|
||||||
const viewportTop = windowTop - slack;
|
const viewportTop = windowTop - slack;
|
||||||
|
@ -79,7 +83,18 @@ export default MountWidget.extend({
|
||||||
if (windowBottom > bodyHeight) { windowBottom = bodyHeight; }
|
if (windowBottom > bodyHeight) { windowBottom = bodyHeight; }
|
||||||
if (viewportBottom > bodyHeight) { viewportBottom = bodyHeight; }
|
if (viewportBottom > bodyHeight) { viewportBottom = bodyHeight; }
|
||||||
|
|
||||||
|
let currentPost = null;
|
||||||
|
let percent = null;
|
||||||
|
|
||||||
|
const offset = offsetCalculator();
|
||||||
|
const topCheck = Math.ceil(windowTop + offset);
|
||||||
|
|
||||||
|
// uncomment to debug the eyeline
|
||||||
|
// $('.debug-eyeline').css({ height: '1px', width: '100%', backgroundColor: 'blue', position: 'absolute', top: `${topCheck}px` });
|
||||||
|
|
||||||
|
let allAbove = true;
|
||||||
let bottomView = topView;
|
let bottomView = topView;
|
||||||
|
let lastBottom = 0;
|
||||||
while (bottomView < $posts.length) {
|
while (bottomView < $posts.length) {
|
||||||
const post = $posts[bottomView];
|
const post = $posts[bottomView];
|
||||||
const $post = $(post);
|
const $post = $(post);
|
||||||
|
@ -87,18 +102,34 @@ export default MountWidget.extend({
|
||||||
if (!$post) { break; }
|
if (!$post) { break; }
|
||||||
|
|
||||||
const viewTop = $post.offset().top;
|
const viewTop = $post.offset().top;
|
||||||
const viewBottom = viewTop + $post.height() + 100;
|
const postHeight = $post.height();
|
||||||
|
const viewBottom = Math.ceil(viewTop + postHeight);
|
||||||
|
|
||||||
|
allAbove = allAbove && (viewTop < topCheck);
|
||||||
|
|
||||||
if (viewTop > viewportBottom) { break; }
|
if (viewTop > viewportBottom) { break; }
|
||||||
|
|
||||||
if (viewBottom > windowTop && viewTop <= windowBottom) {
|
if (viewBottom >= windowTop && viewTop <= windowBottom) {
|
||||||
onscreen.push(bottomView);
|
onscreen.push(bottomView);
|
||||||
}
|
}
|
||||||
nearby.push(bottomView);
|
|
||||||
|
|
||||||
|
if ((currentPost === null) &&
|
||||||
|
((viewTop <= topCheck && viewBottom >= topCheck) ||
|
||||||
|
(lastBottom <= topCheck && viewTop >= topCheck))) {
|
||||||
|
percent = (topCheck - viewTop) / postHeight;
|
||||||
|
currentPost = bottomView;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastBottom = viewBottom;
|
||||||
|
nearby.push(bottomView);
|
||||||
bottomView++;
|
bottomView++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (allAbove) {
|
||||||
|
if (percent === null) { percent = 1.0; }
|
||||||
|
if (currentPost === null) { currentPost = bottomView - 1; }
|
||||||
|
}
|
||||||
|
|
||||||
const posts = this.posts;
|
const posts = this.posts;
|
||||||
const refresh = cb => this.queueRerender(cb);
|
const refresh = cb => this.queueRerender(cb);
|
||||||
if (onscreen.length) {
|
if (onscreen.length) {
|
||||||
|
@ -131,9 +162,28 @@ export default MountWidget.extend({
|
||||||
this._bottomVisible = last;
|
this._bottomVisible = last;
|
||||||
this.sendAction('bottomVisibleChanged', { post: last, refresh });
|
this.sendAction('bottomVisibleChanged', { post: last, refresh });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changedPost = this._currentPost !== currentPost;
|
||||||
|
if (changedPost) {
|
||||||
|
this._currentPost = currentPost;
|
||||||
|
const post = posts.objectAt(currentPost);
|
||||||
|
this.sendAction('currentPostChanged', { post });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (percent !== null) {
|
||||||
|
if (percent > 1.0) { percent = 1.0; }
|
||||||
|
|
||||||
|
if (changedPost || (this._currentPercent !== percent)) {
|
||||||
|
this._currentPercent = percent;
|
||||||
|
this.sendAction('currentPostScrolled', { percent });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this._topVisible = null;
|
this._topVisible = null;
|
||||||
this._bottomVisible = null;
|
this._bottomVisible = null;
|
||||||
|
this._currentPost = null;
|
||||||
|
this._currentPercent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onscreenPostNumbers = [];
|
const onscreenPostNumbers = [];
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import DButton from 'discourse/components/d-button';
|
|
||||||
|
|
||||||
export default DButton.extend({
|
|
||||||
click() {
|
|
||||||
const $target = this.$(),
|
|
||||||
position = $target.position(),
|
|
||||||
width = $target.innerWidth(),
|
|
||||||
loc = {
|
|
||||||
position: this.get('position') || "fixed",
|
|
||||||
left: position.left + width,
|
|
||||||
top: position.top
|
|
||||||
};
|
|
||||||
|
|
||||||
this.appEvents.trigger("popup-menu:open", loc);
|
|
||||||
this.sendAction("action");
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,5 +1,6 @@
|
||||||
import MountWidget from 'discourse/components/mount-widget';
|
import MountWidget from 'discourse/components/mount-widget';
|
||||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
import Docking from 'discourse/mixins/docking';
|
||||||
|
|
||||||
const _flagProperties = [];
|
const _flagProperties = [];
|
||||||
function addFlagProperty(prop) {
|
function addFlagProperty(prop) {
|
||||||
|
@ -8,45 +9,37 @@ function addFlagProperty(prop) {
|
||||||
|
|
||||||
const PANEL_BODY_MARGIN = 30;
|
const PANEL_BODY_MARGIN = 30;
|
||||||
|
|
||||||
const SiteHeaderComponent = MountWidget.extend({
|
const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||||
widget: 'header',
|
widget: 'header',
|
||||||
docAt: null,
|
docAt: null,
|
||||||
dockedHeader: null,
|
dockedHeader: null,
|
||||||
_topic: null,
|
_topic: null,
|
||||||
|
|
||||||
// profileWidget: true,
|
|
||||||
// classNameBindings: ['editingTopic'],
|
|
||||||
|
|
||||||
@observes('currentUser.unread_notifications', 'currentUser.unread_private_messages')
|
@observes('currentUser.unread_notifications', 'currentUser.unread_private_messages')
|
||||||
_notificationsChanged() {
|
_notificationsChanged() {
|
||||||
this.queueRerender();
|
this.queueRerender();
|
||||||
},
|
},
|
||||||
|
|
||||||
examineDockHeader() {
|
dockCheck(info) {
|
||||||
|
if (this.docAt === null) {
|
||||||
|
const outlet = $('#main-outlet');
|
||||||
|
if (!(outlet && outlet.length === 1)) return;
|
||||||
|
this.docAt = outlet.offset().top;
|
||||||
|
}
|
||||||
|
|
||||||
const $body = $('body');
|
const $body = $('body');
|
||||||
|
const offset = info.offset();
|
||||||
// Check the dock after the current run loop. While rendering,
|
if (offset >= this.docAt) {
|
||||||
// it's much slower to calculate `outlet.offset()`
|
if (!this.dockedHeader) {
|
||||||
Ember.run.next(() => {
|
$body.addClass('docked');
|
||||||
if (this.docAt === null) {
|
this.dockedHeader = true;
|
||||||
const outlet = $('#main-outlet');
|
|
||||||
if (!(outlet && outlet.length === 1)) return;
|
|
||||||
this.docAt = outlet.offset().top;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
const offset = window.pageYOffset || $('html').scrollTop();
|
if (this.dockedHeader) {
|
||||||
if (offset >= this.docAt) {
|
$body.removeClass('docked');
|
||||||
if (!this.dockedHeader) {
|
this.dockedHeader = false;
|
||||||
$body.addClass('docked');
|
|
||||||
this.dockedHeader = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.dockedHeader) {
|
|
||||||
$body.removeClass('docked');
|
|
||||||
this.dockedHeader = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setTopic(topic) {
|
setTopic(topic) {
|
||||||
|
@ -56,8 +49,6 @@ const SiteHeaderComponent = MountWidget.extend({
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super();
|
this._super();
|
||||||
$(window).bind('scroll.discourse-dock', () => this.examineDockHeader());
|
|
||||||
$(document).bind('touchmove.discourse-dock', () => this.examineDockHeader());
|
|
||||||
$(window).on('resize.discourse-menu-panel', () => this.afterRender());
|
$(window).on('resize.discourse-menu-panel', () => this.afterRender());
|
||||||
|
|
||||||
this.appEvents.on('header:show-topic', topic => this.setTopic(topic));
|
this.appEvents.on('header:show-topic', topic => this.setTopic(topic));
|
||||||
|
@ -72,16 +63,11 @@ const SiteHeaderComponent = MountWidget.extend({
|
||||||
this.eventDispatched('dom:clean', 'header');
|
this.eventDispatched('dom:clean', 'header');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.examineDockHeader();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
this._super();
|
this._super();
|
||||||
$(window).unbind('scroll.discourse-dock');
|
|
||||||
$(document).unbind('touchmove.discourse-dock');
|
|
||||||
$('body').off('keydown.header');
|
$('body').off('keydown.header');
|
||||||
this.appEvents.off('notifications:changed');
|
|
||||||
$(window).off('resize.discourse-menu-panel');
|
$(window).off('resize.discourse-menu-panel');
|
||||||
|
|
||||||
this.appEvents.off('header:show-topic');
|
this.appEvents.off('header:show-topic');
|
||||||
|
|
|
@ -6,9 +6,9 @@ function formatTag(t) {
|
||||||
|
|
||||||
export default Ember.TextField.extend({
|
export default Ember.TextField.extend({
|
||||||
classNameBindings: [':tag-chooser'],
|
classNameBindings: [':tag-chooser'],
|
||||||
attributeBindings: ['tabIndex'],
|
attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'],
|
||||||
|
|
||||||
_setupTags: function() {
|
_initValue: function() {
|
||||||
const tags = this.get('tags') || [];
|
const tags = this.get('tags') || [];
|
||||||
this.set('value', tags.join(", "));
|
this.set('value', tags.join(", "));
|
||||||
}.on('init'),
|
}.on('init'),
|
||||||
|
@ -18,16 +18,37 @@ export default Ember.TextField.extend({
|
||||||
this.set('tags', tags);
|
this.set('tags', tags);
|
||||||
}.observes('value'),
|
}.observes('value'),
|
||||||
|
|
||||||
|
_tagsChanged: function() {
|
||||||
|
const $tagChooser = this.$(),
|
||||||
|
val = this.get('value');
|
||||||
|
|
||||||
|
if ($tagChooser && val !== this.get('tags')) {
|
||||||
|
if (this.get('tags')) {
|
||||||
|
const data = this.get('tags').map((t) => {return {id: t, text: t};});
|
||||||
|
$tagChooser.select2('data', data);
|
||||||
|
} else {
|
||||||
|
$tagChooser.select2('data', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.observes('tags'),
|
||||||
|
|
||||||
_initializeTags: function() {
|
_initializeTags: function() {
|
||||||
const site = this.site,
|
const site = this.site,
|
||||||
self = this,
|
self = this,
|
||||||
filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
|
filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
|
||||||
|
var limit = this.siteSettings.max_tags_per_topic;
|
||||||
|
|
||||||
|
if (this.get('unlimitedTagCount')) {
|
||||||
|
limit = null;
|
||||||
|
} else if (this.get('limit')) {
|
||||||
|
limit = parseInt(this.get('limit'));
|
||||||
|
}
|
||||||
|
|
||||||
this.$().select2({
|
this.$().select2({
|
||||||
tags: true,
|
tags: true,
|
||||||
placeholder: I18n.t('tagging.choose_for_topic'),
|
placeholder: I18n.t(this.get('placeholderKey') || 'tagging.choose_for_topic'),
|
||||||
maximumInputLength: this.siteSettings.max_tag_length,
|
maximumInputLength: this.siteSettings.max_tag_length,
|
||||||
maximumSelectionSize: this.siteSettings.max_tags_per_topic,
|
maximumSelectionSize: limit,
|
||||||
initSelection(element, callback) {
|
initSelection(element, callback) {
|
||||||
const data = [];
|
const data = [];
|
||||||
|
|
||||||
|
@ -65,7 +86,7 @@ export default Ember.TextField.extend({
|
||||||
list.push(item);
|
list.push(item);
|
||||||
},
|
},
|
||||||
formatSelection: function (data) {
|
formatSelection: function (data) {
|
||||||
return data ? renderTag(this.text(data)) : undefined;
|
return data ? renderTag(this.text(data)) : undefined;
|
||||||
},
|
},
|
||||||
formatSelectionCssClass: function(){
|
formatSelectionCssClass: function(){
|
||||||
return "discourse-tag-select2";
|
return "discourse-tag-select2";
|
||||||
|
@ -78,7 +99,16 @@ export default Ember.TextField.extend({
|
||||||
url: Discourse.getURL("/tags/filter/search"),
|
url: Discourse.getURL("/tags/filter/search"),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
data: function (term) {
|
data: function (term) {
|
||||||
return { q: term, limit: self.siteSettings.max_tag_search_results };
|
const d = {
|
||||||
|
q: term,
|
||||||
|
limit: self.siteSettings.max_tag_search_results,
|
||||||
|
categoryId: self.get('categoryId'),
|
||||||
|
selected_tags: self.get('tags')
|
||||||
|
};
|
||||||
|
if (!self.get('everyTag')) {
|
||||||
|
d.filterForInput = true;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
},
|
},
|
||||||
results: function (data) {
|
results: function (data) {
|
||||||
if (self.siteSettings.tags_sort_alphabetically) {
|
if (self.siteSettings.tags_sort_alphabetically) {
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
function renderTagGroup(tag) {
|
||||||
|
return "<a class='discourse-tag'>" + Handlebars.Utils.escapeExpression(tag.text ? tag.text : tag) + "</a>";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Ember.TextField.extend({
|
||||||
|
classNameBindings: [':tag-chooser'],
|
||||||
|
attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'],
|
||||||
|
|
||||||
|
_initValue: function() {
|
||||||
|
const names = this.get('tagGroups') || [];
|
||||||
|
this.set('value', names.join(", "));
|
||||||
|
}.on('init'),
|
||||||
|
|
||||||
|
_valueChanged: function() {
|
||||||
|
const names = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq();
|
||||||
|
this.set('tagGroups', names);
|
||||||
|
}.observes('value'),
|
||||||
|
|
||||||
|
_tagGroupsChanged: function() {
|
||||||
|
const $chooser = this.$(),
|
||||||
|
val = this.get('value');
|
||||||
|
|
||||||
|
if ($chooser && val !== this.get('tagGroups')) {
|
||||||
|
if (this.get('tagGroups')) {
|
||||||
|
const data = this.get('tagGroups').map((t) => {return {id: t, text: t};});
|
||||||
|
$chooser.select2('data', data);
|
||||||
|
} else {
|
||||||
|
$chooser.select2('data', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.observes('tagGroups'),
|
||||||
|
|
||||||
|
_initializeChooser: function() {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
this.$().select2({
|
||||||
|
tags: true,
|
||||||
|
placeholder: this.get('placeholderKey') ? I18n.t(this.get('placeholderKey')) : null,
|
||||||
|
initSelection(element, callback) {
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
function splitVal(string, separator) {
|
||||||
|
var val, i, l;
|
||||||
|
if (string === null || string.length < 1) return [];
|
||||||
|
val = string.split(separator);
|
||||||
|
for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(splitVal(element.val(), ",")).each(function () {
|
||||||
|
data.push({ id: this, text: this });
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(data);
|
||||||
|
},
|
||||||
|
formatSelection: function (data) {
|
||||||
|
return data ? renderTagGroup(this.text(data)) : undefined;
|
||||||
|
},
|
||||||
|
formatSelectionCssClass: function(){
|
||||||
|
return "discourse-tag-select2";
|
||||||
|
},
|
||||||
|
formatResult: renderTagGroup,
|
||||||
|
multiple: true,
|
||||||
|
ajax: {
|
||||||
|
quietMillis: 200,
|
||||||
|
cache: true,
|
||||||
|
url: Discourse.getURL("/tag_groups/filter/search"),
|
||||||
|
dataType: 'json',
|
||||||
|
data: function (term) {
|
||||||
|
return { q: term, limit: self.siteSettings.max_tag_search_results };
|
||||||
|
},
|
||||||
|
results: function (data) {
|
||||||
|
data.results = data.results.sort(function(a,b) { return a.text > b.text; });
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}.on('didInsertElement'),
|
||||||
|
|
||||||
|
_destroyChooser: function() {
|
||||||
|
this.$().select2('destroy');
|
||||||
|
}.on('willDestroyElement')
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNameBindings: [':tag-list', 'categoryClass'],
|
||||||
|
|
||||||
|
sortedTags: Ember.computed.sort('tags', 'sortProperties'),
|
||||||
|
|
||||||
|
title: function() {
|
||||||
|
if (this.get('titleKey')) { return I18n.t(this.get('titleKey')); }
|
||||||
|
}.property('titleKey'),
|
||||||
|
|
||||||
|
category: function() {
|
||||||
|
if (this.get('categoryId')) {
|
||||||
|
return Discourse.Category.findById(this.get('categoryId'));
|
||||||
|
}
|
||||||
|
}.property('categoryId'),
|
||||||
|
|
||||||
|
categoryClass: function() {
|
||||||
|
if (this.get('category')) {
|
||||||
|
return "tag-list-" + this.get('category.fullSlug');
|
||||||
|
}
|
||||||
|
}.property('category')
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||||
|
import DropdownButton from 'discourse/components/dropdown-button';
|
||||||
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
|
||||||
|
export default DropdownButton.extend({
|
||||||
|
buttonExtraClasses: 'no-text',
|
||||||
|
title: '',
|
||||||
|
text: iconHTML('bars') + ' ' + iconHTML('caret-down'),
|
||||||
|
classNames: ['tags-admin-menu'],
|
||||||
|
|
||||||
|
@computed()
|
||||||
|
dropDownContent() {
|
||||||
|
const items = [
|
||||||
|
{ id: 'manageGroups',
|
||||||
|
title: I18n.t('tagging.manage_groups'),
|
||||||
|
description: I18n.t('tagging.manage_groups_description'),
|
||||||
|
styleClasses: 'fa fa-wrench' }
|
||||||
|
];
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
|
||||||
|
actionNames: {
|
||||||
|
manageGroups: 'showTagGroups'
|
||||||
|
},
|
||||||
|
|
||||||
|
clicked(id) {
|
||||||
|
this.sendAction('actionNames.' + id);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
import MountWidget from 'discourse/components/mount-widget';
|
||||||
|
|
||||||
|
export default MountWidget.extend({
|
||||||
|
tagName: 'span',
|
||||||
|
widget: "topic-admin-menu-button",
|
||||||
|
|
||||||
|
buildArgs() {
|
||||||
|
return this.getProperties('topic', 'fixed', 'openUpwards');
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,16 +1,15 @@
|
||||||
import ContainerView from 'discourse/views/container';
|
import ContainerView from 'discourse/views/container';
|
||||||
import { on } from 'ember-addons/ember-computed-decorators';
|
|
||||||
|
|
||||||
export default ContainerView.extend({
|
export default ContainerView.extend({
|
||||||
elementId: 'topic-footer-buttons',
|
elementId: 'topic-footer-buttons',
|
||||||
|
|
||||||
@on('init')
|
init() {
|
||||||
createButtons() {
|
this._super();
|
||||||
const topic = this.get('topic');
|
|
||||||
const currentUser = this.get('controller.currentUser');
|
if (this.currentUser) {
|
||||||
|
const viewArgs = this.getProperties('topic', 'topicDelegated');
|
||||||
|
viewArgs.currentUser = this.currentUser;
|
||||||
|
|
||||||
if (currentUser) {
|
|
||||||
const viewArgs = { topic, currentUser };
|
|
||||||
this.attachViewWithArgs(viewArgs, 'topic-footer-main-buttons');
|
this.attachViewWithArgs(viewArgs, 'topic-footer-main-buttons');
|
||||||
this.attachViewWithArgs(viewArgs, 'pinned-button');
|
this.attachViewWithArgs(viewArgs, 'pinned-button');
|
||||||
this.attachViewWithArgs(viewArgs, 'topic-notifications-button');
|
this.attachViewWithArgs(viewArgs, 'topic-notifications-button');
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
composerOpen: null,
|
||||||
|
classNameBindings: ['composerOpen'],
|
||||||
|
showTimeline: null,
|
||||||
|
info: null,
|
||||||
|
|
||||||
|
_checkSize() {
|
||||||
|
const renderTimeline = $(window).width() > 960;
|
||||||
|
this.set('info', { renderTimeline, showTimeline: renderTimeline && !this.get('composerOpen') });
|
||||||
|
},
|
||||||
|
|
||||||
|
composerOpened() {
|
||||||
|
this.set('composerOpen', true);
|
||||||
|
this._checkSize();
|
||||||
|
},
|
||||||
|
|
||||||
|
composerClosed() {
|
||||||
|
this.set('composerOpen', false);
|
||||||
|
this._checkSize();
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
|
||||||
|
if (!this.site.mobileView) {
|
||||||
|
$(window).on('resize.discourse-topic-navigation', () => this._checkSize());
|
||||||
|
this.appEvents.on('composer:will-open', this, this.composerOpened);
|
||||||
|
this.appEvents.on('composer:will-close', this, this.composerClosed);
|
||||||
|
this._checkSize();
|
||||||
|
} else {
|
||||||
|
this.set('info', null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
this._super();
|
||||||
|
if (!this.site.mobileView) {
|
||||||
|
$(window).off('resize.discourse-topic-navigation');
|
||||||
|
this.appEvents.off('composer:will-open', this, this.composerOpened);
|
||||||
|
this.appEvents.off('composer:will-close', this, this.composerClosed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,16 +1,15 @@
|
||||||
import NotificationsButton from 'discourse/components/notifications-button';
|
import MountWidget from 'discourse/components/mount-widget';
|
||||||
|
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
export default NotificationsButton.extend({
|
export default MountWidget.extend({
|
||||||
longDescription: Em.computed.alias('topic.details.notificationReasonText'),
|
widget: 'topic-notifications-button',
|
||||||
hidden: Em.computed.alias('topic.deleted'),
|
|
||||||
notificationLevel: Em.computed.alias('topic.details.notification_level'),
|
|
||||||
i18nPrefix: 'topic.notifications',
|
|
||||||
|
|
||||||
i18nPostfix: function() {
|
buildArgs() {
|
||||||
return this.get('topic.isPrivateMessage') ? '_pm' : '';
|
return { topic: this.get('topic'), appendReason: true, showFullTitle: true };
|
||||||
}.property('topic.isPrivateMessage'),
|
},
|
||||||
|
|
||||||
clicked(id) {
|
@observes('topic.details.notification_level')
|
||||||
this.get('topic.details').updateNotifications(id);
|
_triggerRerender() {
|
||||||
|
this.queueRerender();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,219 @@
|
||||||
|
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
elementId: 'topic-progress-wrapper',
|
||||||
|
classNameBindings: ['docked', 'hidden'],
|
||||||
|
expanded: false,
|
||||||
|
toPostIndex: null,
|
||||||
|
docked: false,
|
||||||
|
progressPosition: null,
|
||||||
|
postStream: Ember.computed.alias('topic.postStream'),
|
||||||
|
userWantsToJump: null,
|
||||||
|
_streamPercentage: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super();
|
||||||
|
(this.get('delegated') || []).forEach(m => this.set(m, m));
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('userWantsToJump', 'showTimeline')
|
||||||
|
hidden(userWantsToJump, showTimeline) {
|
||||||
|
return !userWantsToJump && showTimeline;
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes('hidden')
|
||||||
|
visibilityChanged() {
|
||||||
|
if (!this.get('hidden')) {
|
||||||
|
this._updateBar();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
keyboardTrigger(kbdEvent) {
|
||||||
|
if (kbdEvent.type === 'jump') {
|
||||||
|
this.set('expanded', true);
|
||||||
|
this.set('userWantsToJump', true);
|
||||||
|
Ember.run.scheduleOnce('afterRender', () => this.$('.jump-form input').focus());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('progressPosition')
|
||||||
|
jumpTopDisabled(progressPosition) {
|
||||||
|
return progressPosition <= 3;
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('postStream.filteredPostsCount', 'topic.highest_post_number', 'progressPosition')
|
||||||
|
jumpBottomDisabled(filteredPostsCount, highestPostNumber, progressPosition) {
|
||||||
|
return progressPosition >= filteredPostsCount || progressPosition >= highestPostNumber;
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('postStream.loaded', 'topic.currentPost', 'postStream.filteredPostsCount')
|
||||||
|
hideProgress(loaded, currentPost, filteredPostsCount) {
|
||||||
|
return (!loaded) || (!currentPost) || (filteredPostsCount < 2);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('postStream.filteredPostsCount')
|
||||||
|
hugeNumberOfPosts(filteredPostsCount) {
|
||||||
|
return filteredPostsCount >= this.siteSettings.short_progress_text_threshold;
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('hugeNumberOfPosts', 'topic.highest_post_number')
|
||||||
|
jumpToBottomTitle(hugeNumberOfPosts, highestPostNumber) {
|
||||||
|
if (hugeNumberOfPosts) {
|
||||||
|
return I18n.t('topic.progress.jump_bottom_with_number', { post_number: highestPostNumber });
|
||||||
|
} else {
|
||||||
|
return I18n.t('topic.progress.jump_bottom');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes('postStream.stream.[]')
|
||||||
|
_updateBar() {
|
||||||
|
Ember.run.scheduleOnce('afterRender', this, this._updateProgressBar);
|
||||||
|
},
|
||||||
|
|
||||||
|
_topicScrolled(event) {
|
||||||
|
this.set('progressPosition', event.postIndex);
|
||||||
|
this._streamPercentage = event.percent;
|
||||||
|
this._updateBar();
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
|
||||||
|
this.appEvents.on('composer:will-open', this, this._dock)
|
||||||
|
.on("composer:resized", this, this._dock)
|
||||||
|
.on('composer:closed', this, this._dock)
|
||||||
|
.on("topic:scrolled", this, this._dock)
|
||||||
|
.on('topic:current-post-scrolled', this, this._topicScrolled)
|
||||||
|
.on('topic-progress:keyboard-trigger', this, this.keyboardTrigger);
|
||||||
|
|
||||||
|
Ember.run.scheduleOnce('afterRender', this, this._updateProgressBar);
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
this._super();
|
||||||
|
this.appEvents.off('composer:will-open', this, this._dock)
|
||||||
|
.off("composer:resized", this, this._dock)
|
||||||
|
.off('composer:closed', this, this._dock)
|
||||||
|
.off('topic:scrolled', this, this._dock)
|
||||||
|
.off('topic:current-post-scrolled', this, this._topicScrolled)
|
||||||
|
.off('topic-progress:keyboard-trigger');
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateProgressBar() {
|
||||||
|
if (this.isDestroyed || this.isDestroying || this.get('hidden')) { return; }
|
||||||
|
|
||||||
|
const $topicProgress = this.$('#topic-progress');
|
||||||
|
// speeds up stuff, bypass jquery slowness and extra checks
|
||||||
|
if (!this._totalWidth) {
|
||||||
|
this._totalWidth = $topicProgress[0].offsetWidth;
|
||||||
|
}
|
||||||
|
const totalWidth = this._totalWidth;
|
||||||
|
const progressWidth = (this._streamPercentage || 0) * totalWidth;
|
||||||
|
|
||||||
|
const borderSize = (progressWidth === totalWidth) ? "0px" : "1px";
|
||||||
|
const $bg = $topicProgress.find('.bg');
|
||||||
|
if ($bg.length === 0) {
|
||||||
|
const style = `border-right-width: ${borderSize}; width: ${progressWidth}px`;
|
||||||
|
$topicProgress.append(`<div class='bg' style="${style}"> </div>`);
|
||||||
|
} else {
|
||||||
|
$bg.css("border-right-width", borderSize).width(progressWidth);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_dock() {
|
||||||
|
const maximumOffset = $('#topic-footer-buttons').offset(),
|
||||||
|
composerHeight = $('#reply-control').height() || 0,
|
||||||
|
$topicProgressWrapper = this.$(),
|
||||||
|
offset = window.pageYOffset || $('html').scrollTop(),
|
||||||
|
topicProgressHeight = $('#topic-progress').height();
|
||||||
|
|
||||||
|
let isDocked = false;
|
||||||
|
if (maximumOffset) {
|
||||||
|
const threshold = maximumOffset.top;
|
||||||
|
const windowHeight = $(window).height();
|
||||||
|
isDocked = offset >= threshold - windowHeight + topicProgressHeight + composerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dockPos = $(document).height() - $('#topic-bottom').offset().top;
|
||||||
|
if (composerHeight > 0) {
|
||||||
|
if (isDocked) {
|
||||||
|
$topicProgressWrapper.css('bottom', dockPos);
|
||||||
|
} else {
|
||||||
|
const height = composerHeight + "px";
|
||||||
|
if ($topicProgressWrapper.css('bottom') !== height) {
|
||||||
|
$topicProgressWrapper.css('bottom', height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$topicProgressWrapper.css('bottom', isDocked ? dockPos : '');
|
||||||
|
}
|
||||||
|
this.set('docked', isDocked);
|
||||||
|
},
|
||||||
|
|
||||||
|
click(e) {
|
||||||
|
if ($(e.target).parents('#topic-progress').length) {
|
||||||
|
this.send('toggleExpansion');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
keyDown(e) {
|
||||||
|
if (this.get('expanded')) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
this.$('input').blur();
|
||||||
|
this.send('jumpPost');
|
||||||
|
} else if (e.keyCode === 27) {
|
||||||
|
this.send('toggleExpansion');
|
||||||
|
this.set('userWantsToJump', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
toggleExpansion(opts) {
|
||||||
|
this.toggleProperty('expanded');
|
||||||
|
if (this.get('expanded')) {
|
||||||
|
this.set('userWantsToJump', false);
|
||||||
|
this.set('toPostIndex', this.get('progressPosition'));
|
||||||
|
if(opts && opts.highlight){
|
||||||
|
// TODO: somehow move to view?
|
||||||
|
Em.run.next(function(){
|
||||||
|
$('.jump-form input').select().focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.site.mobileView && !this.capabilities.isIOS) {
|
||||||
|
Ember.run.schedule('afterRender', () => this.$('input').focus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
jumpPost() {
|
||||||
|
let postIndex = parseInt(this.get('toPostIndex'), 10);
|
||||||
|
|
||||||
|
// Validate the post index first
|
||||||
|
if (isNaN(postIndex) || postIndex < 1) {
|
||||||
|
postIndex = 1;
|
||||||
|
}
|
||||||
|
if (postIndex > this.get('postStream.filteredPostsCount')) {
|
||||||
|
postIndex = this.get('postStream.filteredPostsCount');
|
||||||
|
}
|
||||||
|
this.set('toPostIndex', postIndex);
|
||||||
|
this._beforeJump();
|
||||||
|
this.sendAction('jumpToIndex', postIndex);
|
||||||
|
},
|
||||||
|
|
||||||
|
jumpTop() {
|
||||||
|
this._beforeJump();
|
||||||
|
this.sendAction('jumpTop');
|
||||||
|
},
|
||||||
|
|
||||||
|
jumpBottom() {
|
||||||
|
this._beforeJump();
|
||||||
|
this.sendAction('jumpBottom');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_beforeJump() {
|
||||||
|
this.set('expanded', false);
|
||||||
|
this.set('userWantsToJump', false);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,59 @@
|
||||||
|
import MountWidget from 'discourse/components/mount-widget';
|
||||||
|
import Docking from 'discourse/mixins/docking';
|
||||||
|
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
const FIXED_POS = 85;
|
||||||
|
|
||||||
|
export default MountWidget.extend(Docking, {
|
||||||
|
widget: 'topic-timeline-container',
|
||||||
|
dockBottom: null,
|
||||||
|
dockAt: null,
|
||||||
|
|
||||||
|
buildArgs() {
|
||||||
|
return { topic: this.get('topic'),
|
||||||
|
topicTrackingState: this.topicTrackingState,
|
||||||
|
enteredIndex: this.get('enteredIndex'),
|
||||||
|
dockAt: this.dockAt,
|
||||||
|
top: this.dockAt || FIXED_POS,
|
||||||
|
dockBottom: this.dockBottom };
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes('topic.highest_post_number', 'loading')
|
||||||
|
newPostAdded() {
|
||||||
|
this.queueRerender(() => this.queueDockCheck());
|
||||||
|
},
|
||||||
|
|
||||||
|
dockCheck(info) {
|
||||||
|
const mainOffset = $('#main').offset();
|
||||||
|
const offsetTop = mainOffset ? mainOffset.top : 0;
|
||||||
|
const topicTop = $('.container.posts').offset().top - offsetTop;
|
||||||
|
const topicBottom = $('#topic-bottom').offset().top;
|
||||||
|
const $timeline = this.$('.timeline-container');
|
||||||
|
const timelineHeight = $timeline.height() || 400;
|
||||||
|
const footerHeight = $('.timeline-footer-controls').outerHeight(true) || 0;
|
||||||
|
|
||||||
|
const prev = this.dockAt;
|
||||||
|
const posTop = FIXED_POS + info.offset();
|
||||||
|
const pos = posTop + timelineHeight;
|
||||||
|
|
||||||
|
this.dockBottom = false;
|
||||||
|
if (posTop < topicTop) {
|
||||||
|
this.dockAt = topicTop;
|
||||||
|
} else if (pos > topicBottom + footerHeight) {
|
||||||
|
this.dockAt = (topicBottom - timelineHeight) + footerHeight;
|
||||||
|
this.dockBottom = true;
|
||||||
|
if (this.dockAt < 0) { this.dockAt = 0; }
|
||||||
|
} else {
|
||||||
|
this.dockAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dockAt !== prev) {
|
||||||
|
this.queueRerender();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
this.dispatch('topic:current-post-scrolled', 'timeline-scrollarea');
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,81 +0,0 @@
|
||||||
// A controller for displaying messages as the user composes a message.
|
|
||||||
export default Ember.ArrayController.extend({
|
|
||||||
needs: ['composer'],
|
|
||||||
|
|
||||||
// Whether we've checked our messages
|
|
||||||
checkedMessages: false,
|
|
||||||
|
|
||||||
_init: function() {
|
|
||||||
this.reset();
|
|
||||||
}.on("init"),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
closeMessage(message) {
|
|
||||||
this.removeObject(message);
|
|
||||||
},
|
|
||||||
|
|
||||||
hideMessage(message) {
|
|
||||||
this.removeObject(message);
|
|
||||||
// kind of hacky but the visibility depends on this
|
|
||||||
this.get('messagesByTemplate')[message.get('templateName')] = undefined;
|
|
||||||
},
|
|
||||||
|
|
||||||
popup(message) {
|
|
||||||
let messagesByTemplate = this.get('messagesByTemplate');
|
|
||||||
const templateName = message.get('templateName');
|
|
||||||
|
|
||||||
if (!messagesByTemplate[templateName]) {
|
|
||||||
this.pushObject(message);
|
|
||||||
messagesByTemplate[templateName] = message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Resets all active messages.
|
|
||||||
// For example if composing a new post.
|
|
||||||
reset() {
|
|
||||||
this.clear();
|
|
||||||
this.setProperties({
|
|
||||||
messagesByTemplate: {},
|
|
||||||
queuedForTyping: [],
|
|
||||||
checkedMessages: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Called after the user has typed a reply.
|
|
||||||
// Some messages only get shown after being typed.
|
|
||||||
typedReply() {
|
|
||||||
this.get('queuedForTyping').forEach(msg => this.send("popup", msg));
|
|
||||||
},
|
|
||||||
|
|
||||||
groupsMentioned(groups) {
|
|
||||||
// reset existing messages, this should always win it is critical
|
|
||||||
this.reset();
|
|
||||||
groups.forEach(group => {
|
|
||||||
const msg = I18n.t('composer.group_mentioned', {
|
|
||||||
group: "@" + group.name,
|
|
||||||
count: group.user_count,
|
|
||||||
group_link: Discourse.getURL(`/group/${group.name}/members`)
|
|
||||||
});
|
|
||||||
this.send("popup",
|
|
||||||
Em.Object.create({
|
|
||||||
templateName: 'composer/group-mentioned',
|
|
||||||
body: msg})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Figure out if there are any messages that should be displayed above the composer.
|
|
||||||
queryFor(composer) {
|
|
||||||
if (this.get('checkedMessages')) { return; }
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
var queuedForTyping = self.get('queuedForTyping');
|
|
||||||
|
|
||||||
Discourse.ComposerMessage.find(composer).then(messages => {
|
|
||||||
self.set('checkedMessages', true);
|
|
||||||
messages.forEach(msg => msg.wait_for_typing ? queuedForTyping.addObject(msg) : self.send("popup", msg));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
|
@ -3,6 +3,7 @@ import Quote from 'discourse/lib/quote';
|
||||||
import Draft from 'discourse/models/draft';
|
import Draft from 'discourse/models/draft';
|
||||||
import Composer from 'discourse/models/composer';
|
import Composer from 'discourse/models/composer';
|
||||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
import { relativeAge } from 'discourse/lib/formatter';
|
||||||
|
|
||||||
function loadDraft(store, opts) {
|
function loadDraft(store, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
@ -41,22 +42,39 @@ function loadDraft(store, opts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
const _popupMenuOptionsCallbacks = [];
|
||||||
needs: ['modal', 'topic', 'composer-messages', 'application'],
|
|
||||||
|
|
||||||
|
export function addPopupMenuOptionsCallback(callback) {
|
||||||
|
_popupMenuOptionsCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
needs: ['modal', 'topic', 'application'],
|
||||||
replyAsNewTopicDraft: Em.computed.equal('model.draftKey', Composer.REPLY_AS_NEW_TOPIC_KEY),
|
replyAsNewTopicDraft: Em.computed.equal('model.draftKey', Composer.REPLY_AS_NEW_TOPIC_KEY),
|
||||||
checkedMessages: false,
|
checkedMessages: false,
|
||||||
|
messageCount: null,
|
||||||
showEditReason: false,
|
showEditReason: false,
|
||||||
editReason: null,
|
editReason: null,
|
||||||
scopedCategoryId: null,
|
scopedCategoryId: null,
|
||||||
similarTopics: null,
|
|
||||||
similarTopicsMessage: null,
|
|
||||||
lastSimilaritySearch: null,
|
|
||||||
optionsVisible: false,
|
optionsVisible: false,
|
||||||
lastValidatedAt: null,
|
lastValidatedAt: null,
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
topic: null,
|
topic: null,
|
||||||
|
linkLookup: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super();
|
||||||
|
|
||||||
|
addPopupMenuOptionsCallback(function() {
|
||||||
|
return {
|
||||||
|
action: 'toggleWhisper',
|
||||||
|
icon: 'eye-slash',
|
||||||
|
label: 'composer.toggle_whisper',
|
||||||
|
condition: "canWhisper"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
showToolbar: Em.computed({
|
showToolbar: Em.computed({
|
||||||
get(){
|
get(){
|
||||||
const keyValueStore = this.container.lookup('key-value-store:main');
|
const keyValueStore = this.container.lookup('key-value-store:main');
|
||||||
|
@ -79,10 +97,6 @@ export default Ember.Controller.extend({
|
||||||
|
|
||||||
topicModel: Ember.computed.alias('controllers.topic.model'),
|
topicModel: Ember.computed.alias('controllers.topic.model'),
|
||||||
|
|
||||||
_initializeSimilar: function() {
|
|
||||||
this.set('similarTopics', []);
|
|
||||||
}.on('init'),
|
|
||||||
|
|
||||||
@computed('model.canEditTitle', 'model.creatingPrivateMessage')
|
@computed('model.canEditTitle', 'model.creatingPrivateMessage')
|
||||||
canEditTags(canEditTitle, creatingPrivateMessage) {
|
canEditTags(canEditTitle, creatingPrivateMessage) {
|
||||||
return !this.site.mobileView &&
|
return !this.site.mobileView &&
|
||||||
|
@ -97,6 +111,23 @@ export default Ember.Controller.extend({
|
||||||
return currentUser && currentUser.get('staff') && this.siteSettings.enable_whispers && action === Composer.REPLY;
|
return currentUser && currentUser.get('staff') && this.siteSettings.enable_whispers && action === Composer.REPLY;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@computed("model.composeState")
|
||||||
|
popupMenuOptions(composeState) {
|
||||||
|
if (composeState === 'open') {
|
||||||
|
return _popupMenuOptionsCallbacks.map(callback => {
|
||||||
|
let option = callback();
|
||||||
|
|
||||||
|
if (option.condition) {
|
||||||
|
option.condition = this.get(option.condition);
|
||||||
|
} else {
|
||||||
|
option.condition = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
showWarning: function() {
|
showWarning: function() {
|
||||||
if (!Discourse.User.currentProp('staff')) { return false; }
|
if (!Discourse.User.currentProp('staff')) { return false; }
|
||||||
|
|
||||||
|
@ -111,6 +142,45 @@ export default Ember.Controller.extend({
|
||||||
}.property('model.creatingPrivateMessage', 'model.targetUsernames'),
|
}.property('model.creatingPrivateMessage', 'model.targetUsernames'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
addLinkLookup(linkLookup) {
|
||||||
|
this.set('linkLookup', linkLookup);
|
||||||
|
},
|
||||||
|
|
||||||
|
afterRefresh($preview) {
|
||||||
|
const topic = this.get('model.topic');
|
||||||
|
const linkLookup = this.get('linkLookup');
|
||||||
|
if (!topic || !linkLookup) { return; }
|
||||||
|
|
||||||
|
// Don't check if there's only one post
|
||||||
|
if (topic.get('posts_count') === 1) { return; }
|
||||||
|
|
||||||
|
const post = this.get('model.post');
|
||||||
|
if (post && post.get('user_id') !== this.currentUser.id) { return; }
|
||||||
|
|
||||||
|
const $links = $('a[href]', $preview);
|
||||||
|
$links.each((idx, l) => {
|
||||||
|
const href = $(l).prop('href');
|
||||||
|
if (href && href.length) {
|
||||||
|
const [warn, info] = linkLookup.check(post, href);
|
||||||
|
|
||||||
|
if (warn) {
|
||||||
|
const body = I18n.t('composer.duplicate_link', {
|
||||||
|
domain: info.domain,
|
||||||
|
username: info.username,
|
||||||
|
post_url: topic.urlForPostNumber(info.post_number),
|
||||||
|
ago: relativeAge(moment(info.posted_at).toDate(), { format: 'medium' })
|
||||||
|
});
|
||||||
|
this.appEvents.trigger('composer-messages:create', {
|
||||||
|
extraClass: 'custom-body',
|
||||||
|
templateName: 'custom-body',
|
||||||
|
body
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
toggleWhisper() {
|
toggleWhisper() {
|
||||||
this.toggleProperty('model.whisper');
|
this.toggleProperty('model.whisper');
|
||||||
|
@ -120,7 +190,8 @@ export default Ember.Controller.extend({
|
||||||
this.toggleProperty('showToolbar');
|
this.toggleProperty('showToolbar');
|
||||||
},
|
},
|
||||||
|
|
||||||
showOptions(loc) {
|
showOptions(toolbarEvent, loc) {
|
||||||
|
this.set('toolbarEvent', toolbarEvent);
|
||||||
this.appEvents.trigger('popup-menu:open', loc);
|
this.appEvents.trigger('popup-menu:open', loc);
|
||||||
this.set('optionsVisible', true);
|
this.set('optionsVisible', true);
|
||||||
},
|
},
|
||||||
|
@ -184,9 +255,8 @@ export default Ember.Controller.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
hitEsc() {
|
hitEsc() {
|
||||||
const messages = this.get('controllers.composer-messages.model');
|
if ((this.get('messageCount') || 0) > 0) {
|
||||||
if (messages.length) {
|
this.appEvents.trigger('composer-messages:close');
|
||||||
messages.popObject();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +273,18 @@ export default Ember.Controller.extend({
|
||||||
|
|
||||||
groupsMentioned(groups) {
|
groupsMentioned(groups) {
|
||||||
if (!this.get('model.creatingPrivateMessage') && !this.get('model.topic.isPrivateMessage')) {
|
if (!this.get('model.creatingPrivateMessage') && !this.get('model.topic.isPrivateMessage')) {
|
||||||
this.get('controllers.composer-messages').groupsMentioned(groups);
|
groups.forEach(group => {
|
||||||
|
const body = I18n.t('composer.group_mentioned', {
|
||||||
|
group: "@" + group.name,
|
||||||
|
count: group.user_count,
|
||||||
|
group_link: Discourse.getURL(`/group/${group.name}/members`)
|
||||||
|
});
|
||||||
|
this.appEvents.trigger('composer-messages:create', {
|
||||||
|
extraClass: 'custom-body',
|
||||||
|
templateName: 'custom-body',
|
||||||
|
body
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,7 +425,7 @@ export default Ember.Controller.extend({
|
||||||
|
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
composer.set('disableDrafts', false);
|
composer.set('disableDrafts', false);
|
||||||
self.appEvents.one('composer:opened', () => bootbox.alert(error));
|
self.appEvents.one('composer:will-open', () => bootbox.alert(error));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.get('controllers.application.currentRouteName').split('.')[0] === 'topic' &&
|
if (this.get('controllers.application.currentRouteName').split('.')[0] === 'topic' &&
|
||||||
|
@ -360,61 +441,14 @@ export default Ember.Controller.extend({
|
||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Checks to see if a reply has been typed.
|
// Notify the composer messages controller that a reply has been typed. Some
|
||||||
// This is signaled by a keyUp event in a view.
|
// messages only appear after typing.
|
||||||
checkReplyLength() {
|
checkReplyLength() {
|
||||||
if (!Ember.isEmpty('model.reply')) {
|
if (!Ember.isEmpty('model.reply')) {
|
||||||
// Notify the composer messages controller that a reply has been typed. Some
|
this.appEvents.trigger('composer:typed-reply');
|
||||||
// messages only appear after typing.
|
|
||||||
this.get('controllers.composer-messages').typedReply();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fired after a user stops typing.
|
|
||||||
// Considers whether to check for similar topics based on the current composer state.
|
|
||||||
findSimilarTopics() {
|
|
||||||
// We don't care about similar topics unless creating a topic
|
|
||||||
if (!this.get('model.creatingTopic')) { return; }
|
|
||||||
|
|
||||||
let body = this.get('model.reply') || '';
|
|
||||||
const title = this.get('model.title') || '';
|
|
||||||
|
|
||||||
// Ensure the fields are of the minimum length
|
|
||||||
if (body.length < Discourse.SiteSettings.min_body_similar_length) { return; }
|
|
||||||
if (title.length < Discourse.SiteSettings.min_title_similar_length) { return; }
|
|
||||||
|
|
||||||
// TODO pass the 200 in from somewhere
|
|
||||||
body = body.substr(0, 200);
|
|
||||||
|
|
||||||
// Done search over and over
|
|
||||||
if ((title + body) === this.get('lastSimilaritySearch')) { return; }
|
|
||||||
this.set('lastSimilaritySearch', title + body);
|
|
||||||
|
|
||||||
const messageController = this.get('controllers.composer-messages'),
|
|
||||||
similarTopics = this.get('similarTopics');
|
|
||||||
|
|
||||||
let message = this.get('similarTopicsMessage');
|
|
||||||
if (!message) {
|
|
||||||
message = Discourse.ComposerMessage.create({
|
|
||||||
templateName: 'composer/similar-topics',
|
|
||||||
extraClass: 'similar-topics'
|
|
||||||
});
|
|
||||||
this.set('similarTopicsMessage', message);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.store.find('similar-topic', {title, raw: body}).then(function(newTopics) {
|
|
||||||
similarTopics.clear();
|
|
||||||
similarTopics.pushObjects(newTopics.get('content'));
|
|
||||||
|
|
||||||
if (similarTopics.get('length') > 0) {
|
|
||||||
message.set('similarTopics', similarTopics);
|
|
||||||
messageController.send("popup", message);
|
|
||||||
} else if (message) {
|
|
||||||
messageController.send("hideMessage", message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Open the composer view
|
Open the composer view
|
||||||
|
|
||||||
|
@ -439,13 +473,10 @@ export default Ember.Controller.extend({
|
||||||
this.set('scopedCategoryId', opts.categoryId);
|
this.set('scopedCategoryId', opts.categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const composerMessages = this.get('controllers.composer-messages'),
|
const self = this;
|
||||||
self = this;
|
|
||||||
|
|
||||||
let composerModel = this.get('model');
|
let composerModel = this.get('model');
|
||||||
|
|
||||||
this.setProperties({ showEditReason: false, editReason: null });
|
this.setProperties({ showEditReason: false, editReason: null });
|
||||||
composerMessages.reset();
|
|
||||||
|
|
||||||
// If we want a different draft than the current composer, close it and clear our model.
|
// If we want a different draft than the current composer, close it and clear our model.
|
||||||
if (composerModel &&
|
if (composerModel &&
|
||||||
|
@ -493,6 +524,8 @@ export default Ember.Controller.extend({
|
||||||
|
|
||||||
// Given a potential instance and options, set the model for this composer.
|
// Given a potential instance and options, set the model for this composer.
|
||||||
_setModel(composerModel, opts) {
|
_setModel(composerModel, opts) {
|
||||||
|
this.set('linkLookup', null);
|
||||||
|
|
||||||
if (opts.draft) {
|
if (opts.draft) {
|
||||||
composerModel = loadDraft(this.store, opts);
|
composerModel = loadDraft(this.store, opts);
|
||||||
if (composerModel) {
|
if (composerModel) {
|
||||||
|
@ -532,11 +565,13 @@ export default Ember.Controller.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.topicTags && !this.site.mobileView && this.site.get('can_tag_topics')) {
|
||||||
|
this.set('model.tags', opts.topicTags.split(","));
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.topicBody) {
|
if (opts.topicBody) {
|
||||||
this.set('model.reply', opts.topicBody);
|
this.set('model.reply', opts.topicBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.get('controllers.composer-messages').queryFor(composerModel);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// View a new reply we've made
|
// View a new reply we've made
|
||||||
|
|
|
@ -1,27 +1,50 @@
|
||||||
import { fmt } from 'discourse/lib/computed';
|
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
import Group from 'discourse/models/group';
|
||||||
|
|
||||||
export default Ember.ArrayController.extend({
|
export default Ember.Controller.extend({
|
||||||
needs: ['group'],
|
|
||||||
loading: false,
|
loading: false,
|
||||||
emptyText: fmt('type', 'groups.empty.%@'),
|
limit: null,
|
||||||
|
offset: null,
|
||||||
|
|
||||||
|
@computed('model.owners.[]')
|
||||||
|
isOwner(owners) {
|
||||||
|
if (this.get('currentUser.admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const currentUserId = this.get('currentUser.id');
|
||||||
|
if (currentUserId) {
|
||||||
|
return !!owners.findBy('id', currentUserId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
loadMore() {
|
removeMember(user) {
|
||||||
|
this.get('model').removeMember(user);
|
||||||
|
},
|
||||||
|
|
||||||
if (this.get('loading')) { return; }
|
addMembers() {
|
||||||
this.set('loading', true);
|
const usernames = this.get('usernames');
|
||||||
const posts = this.get('model');
|
if (usernames && usernames.length > 0) {
|
||||||
if (posts && posts.length) {
|
this.get('model').addMembers(usernames).then(() => this.set('usernames', [])).catch(popupAjaxError);
|
||||||
const beforePostId = posts[posts.length-1].get('id');
|
|
||||||
const group = this.get('controllers.group.model');
|
|
||||||
|
|
||||||
const opts = { beforePostId, type: this.get('type') };
|
|
||||||
group.findPosts(opts).then(newPosts => {
|
|
||||||
posts.addObjects(newPosts);
|
|
||||||
this.set('loading', false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadMore() {
|
||||||
|
if (this.get("loading")) { return; }
|
||||||
|
if (this.get("model.members.length") >= this.get("model.user_count")) { return; }
|
||||||
|
|
||||||
|
this.set("loading", true);
|
||||||
|
|
||||||
|
Group.loadMembers(this.get("model.name"), this.get("model.members.length"), this.get("limit")).then(result => {
|
||||||
|
this.get("model.members").addObjects(result.members.map(member => Discourse.User.create(member)));
|
||||||
|
this.setProperties({
|
||||||
|
loading: false,
|
||||||
|
user_count: result.meta.total,
|
||||||
|
limit: result.meta.limit,
|
||||||
|
offset: Math.min(result.meta.offset + result.meta.limit, result.meta.total)
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { fmt } from 'discourse/lib/computed';
|
||||||
|
|
||||||
|
export default Ember.ArrayController.extend({
|
||||||
|
needs: ['group'],
|
||||||
|
loading: false,
|
||||||
|
emptyText: fmt('type', 'groups.empty.%@'),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
loadMore() {
|
||||||
|
|
||||||
|
if (this.get('loading')) { return; }
|
||||||
|
this.set('loading', true);
|
||||||
|
const posts = this.get('model');
|
||||||
|
if (posts && posts.length) {
|
||||||
|
const beforePostId = posts[posts.length-1].get('id');
|
||||||
|
const group = this.get('controllers.group.model');
|
||||||
|
|
||||||
|
const opts = { beforePostId, type: this.get('type') };
|
||||||
|
group.findPosts(opts).then(newPosts => {
|
||||||
|
posts.addObjects(newPosts);
|
||||||
|
this.set('loading', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -15,7 +15,7 @@ var Tab = Em.Object.extend({
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
counts: null,
|
counts: null,
|
||||||
showing: 'posts',
|
showing: 'members',
|
||||||
|
|
||||||
@observes('counts')
|
@observes('counts')
|
||||||
countsChanged() {
|
countsChanged() {
|
||||||
|
@ -35,10 +35,10 @@ export default Ember.Controller.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab.create({ name: 'posts', active: true, 'location': 'group.index' }),
|
Tab.create({ name: 'members', active: true, 'location': 'group.index' }),
|
||||||
|
Tab.create({ name: 'posts' }),
|
||||||
Tab.create({ name: 'topics' }),
|
Tab.create({ name: 'topics' }),
|
||||||
Tab.create({ name: 'mentions' }),
|
Tab.create({ name: 'mentions' }),
|
||||||
Tab.create({ name: 'members' }),
|
|
||||||
Tab.create({ name: 'messages' }),
|
Tab.create({ name: 'messages' }),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
|
||||||
import Group from 'discourse/models/group';
|
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
|
||||||
loading: false,
|
|
||||||
limit: null,
|
|
||||||
offset: null,
|
|
||||||
|
|
||||||
@computed('model.owners.[]')
|
|
||||||
isOwner(owners) {
|
|
||||||
if (this.get('currentUser.admin')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const currentUserId = this.get('currentUser.id');
|
|
||||||
if (currentUserId) {
|
|
||||||
return !!owners.findBy('id', currentUserId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
removeMember(user) {
|
|
||||||
this.get('model').removeMember(user);
|
|
||||||
},
|
|
||||||
|
|
||||||
addMembers() {
|
|
||||||
const usernames = this.get('usernames');
|
|
||||||
if (usernames && usernames.length > 0) {
|
|
||||||
this.get('model').addMembers(usernames).then(() => this.set('usernames', [])).catch(popupAjaxError);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
loadMore() {
|
|
||||||
if (this.get("loading")) { return; }
|
|
||||||
if (this.get("model.members.length") >= this.get("model.user_count")) { return; }
|
|
||||||
|
|
||||||
this.set("loading", true);
|
|
||||||
|
|
||||||
Group.loadMembers(this.get("model.name"), this.get("model.members.length"), this.get("limit")).then(result => {
|
|
||||||
this.get("model.members").addObjects(result.members.map(member => Discourse.User.create(member)));
|
|
||||||
this.setProperties({
|
|
||||||
loading: false,
|
|
||||||
user_count: result.meta.total,
|
|
||||||
limit: result.meta.limit,
|
|
||||||
offset: Math.min(result.meta.offset + result.meta.limit, result.meta.total)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -6,6 +6,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
// If this isn't defined, it will proxy to the user model on the preferences
|
// If this isn't defined, it will proxy to the user model on the preferences
|
||||||
// page which is wrong.
|
// page which is wrong.
|
||||||
emailOrUsername: null,
|
emailOrUsername: null,
|
||||||
|
hasCustomMessage: false,
|
||||||
|
customMessage: null,
|
||||||
inviteIcon: "envelope",
|
inviteIcon: "envelope",
|
||||||
|
|
||||||
isAdmin: function(){
|
isAdmin: function(){
|
||||||
|
@ -27,6 +29,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'),
|
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'),
|
||||||
|
|
||||||
disabledCopyLink: function() {
|
disabledCopyLink: function() {
|
||||||
|
if (this.get('hasCustomMessage')) return true;
|
||||||
if (this.get('model.saving')) return true;
|
if (this.get('model.saving')) return true;
|
||||||
if (Ember.isEmpty(this.get('emailOrUsername'))) return true;
|
if (Ember.isEmpty(this.get('emailOrUsername'))) return true;
|
||||||
const emailOrUsername = this.get('emailOrUsername').trim();
|
const emailOrUsername = this.get('emailOrUsername').trim();
|
||||||
|
@ -37,7 +40,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
// when inviting to private topic via email, group name must be specified
|
// when inviting to private topic via email, group name must be specified
|
||||||
if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && Discourse.Utilities.emailValid(emailOrUsername)) return true;
|
if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && Discourse.Utilities.emailValid(emailOrUsername)) return true;
|
||||||
return false;
|
return false;
|
||||||
}.property('emailOrUsername', 'model.saving', 'isPrivateTopic', 'model.groupNames'),
|
}.property('emailOrUsername', 'model.saving', 'isPrivateTopic', 'model.groupNames', 'hasCustomMessage'),
|
||||||
|
|
||||||
buttonTitle: function() {
|
buttonTitle: function() {
|
||||||
return this.get('model.saving') ? 'topic.inviting' : 'topic.invite_reply.action';
|
return this.get('model.saving') ? 'topic.inviting' : 'topic.invite_reply.action';
|
||||||
|
@ -71,6 +74,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && Discourse.SiteSettings.enable_local_logins && !this.get('isMessage');
|
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && Discourse.SiteSettings.enable_local_logins && !this.get('isMessage');
|
||||||
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic'),
|
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic'),
|
||||||
|
|
||||||
|
showCustomMessage: function() {
|
||||||
|
return (this.get('model') === this.currentUser || Discourse.Utilities.emailValid(this.get('emailOrUsername')));
|
||||||
|
}.property('emailOrUsername'),
|
||||||
|
|
||||||
// Instructional text for the modal.
|
// Instructional text for the modal.
|
||||||
inviteInstructions: function() {
|
inviteInstructions: function() {
|
||||||
if (Discourse.SiteSettings.enable_sso || !Discourse.SiteSettings.enable_local_logins) {
|
if (Discourse.SiteSettings.enable_sso || !Discourse.SiteSettings.enable_local_logins) {
|
||||||
|
@ -102,11 +109,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
}
|
}
|
||||||
}.property('isMessage', 'invitingToTopic', 'emailOrUsername'),
|
}.property('isMessage', 'invitingToTopic', 'emailOrUsername'),
|
||||||
|
|
||||||
// Instructional text for the group selection.
|
showGroupsClass: function() {
|
||||||
groupInstructions: function() {
|
return this.get('isPrivateTopic') ? 'required' : 'optional';
|
||||||
return this.get('isPrivateTopic') ?
|
|
||||||
I18n.t('topic.automatically_add_to_groups_required') :
|
|
||||||
I18n.t('topic.automatically_add_to_groups_optional');
|
|
||||||
}.property('isPrivateTopic'),
|
}.property('isPrivateTopic'),
|
||||||
|
|
||||||
groupFinder(term) {
|
groupFinder(term) {
|
||||||
|
@ -136,9 +140,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
'topic.invite_private.email_or_username_placeholder';
|
'topic.invite_private.email_or_username_placeholder';
|
||||||
}.property(),
|
}.property(),
|
||||||
|
|
||||||
|
customMessagePlaceholder: function() {
|
||||||
|
return I18n.t('invite.custom_message_placeholder');
|
||||||
|
}.property(),
|
||||||
|
|
||||||
// Reset the modal to allow a new user to be invited.
|
// Reset the modal to allow a new user to be invited.
|
||||||
reset() {
|
reset() {
|
||||||
this.set('emailOrUsername', null);
|
this.set('emailOrUsername', null);
|
||||||
|
this.set('hasCustomMessage', false);
|
||||||
|
this.set('customMessage', null);
|
||||||
this.get('model').setProperties({
|
this.get('model').setProperties({
|
||||||
groupNames: null,
|
groupNames: null,
|
||||||
error: false,
|
error: false,
|
||||||
|
@ -147,7 +157,6 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
inviteLink: null
|
inviteLink: null
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
createInvite() {
|
createInvite() {
|
||||||
|
@ -162,7 +171,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
|
|
||||||
model.setProperties({ saving: true, error: false });
|
model.setProperties({ saving: true, error: false });
|
||||||
|
|
||||||
return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames).then(result => {
|
return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames, this.get('customMessage')).then(result => {
|
||||||
model.setProperties({ saving: false, finished: true });
|
model.setProperties({ saving: false, finished: true });
|
||||||
if (!this.get('invitingToTopic')) {
|
if (!this.get('invitingToTopic')) {
|
||||||
Invite.findInvitedBy(this.currentUser, userInvitedController.get('filter')).then(invite_model => {
|
Invite.findInvitedBy(this.currentUser, userInvitedController.get('filter')).then(invite_model => {
|
||||||
|
@ -213,6 +222,19 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
}
|
}
|
||||||
model.setProperties({ saving: false, error: true });
|
model.setProperties({ saving: false, error: true });
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showCustomMessageBox() {
|
||||||
|
this.toggleProperty('hasCustomMessage');
|
||||||
|
if (this.get('hasCustomMessage')) {
|
||||||
|
if (this.get('model') === this.currentUser) {
|
||||||
|
this.set('customMessage', I18n.t('invite.custom_message_template_forum'));
|
||||||
|
} else {
|
||||||
|
this.set('customMessage', I18n.t('invite.custom_message_template_topic'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.set('customMessage', null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -130,21 +130,6 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
checkMailingList(){
|
|
||||||
Em.run.next(()=>{
|
|
||||||
const postsPerDay = this.get('model.mailing_list_posts_per_day');
|
|
||||||
if (!postsPerDay || postsPerDay < 2) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bootbox.confirm(I18n.t("user.enable_mailing_list", {count: postsPerDay}), I18n.t("no_value"), I18n.t("yes_value"), (success) => {
|
|
||||||
if (!success) {
|
|
||||||
this.set('model.user_option.mailing_list_mode', false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
this.set('saved', false);
|
this.set('saved', false);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||||
import BufferedContent from 'discourse/mixins/buffered-content';
|
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||||
|
import { extractError } from 'discourse/lib/ajax-error';
|
||||||
|
|
||||||
export default Ember.Controller.extend(ModalFunctionality, BufferedContent, {
|
export default Ember.Controller.extend(ModalFunctionality, BufferedContent, {
|
||||||
|
|
||||||
|
@ -14,11 +15,15 @@ export default Ember.Controller.extend(ModalFunctionality, BufferedContent, {
|
||||||
performRename() {
|
performRename() {
|
||||||
const tag = this.get('model'),
|
const tag = this.get('model'),
|
||||||
self = this;
|
self = this;
|
||||||
tag.update({ id: this.get('buffered.id') }).then(function() {
|
tag.update({ id: this.get('buffered.id') }).then(function(result) {
|
||||||
self.send('closeModal');
|
self.send('closeModal');
|
||||||
self.transitionToRoute('tags.show', tag.get('id'));
|
if (result.responseJson.tag) {
|
||||||
}).catch(function() {
|
self.transitionToRoute('tags.show', result.responseJson.tag.id);
|
||||||
self.flash(I18n.t('generic_error'), 'error');
|
} else {
|
||||||
|
self.flash(extractError(result.responseJson.errors[0]), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(error) {
|
||||||
|
self.flash(extractError(error), 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ export default Ember.Controller.extend({
|
||||||
needs: ['topic'],
|
needs: ['topic'],
|
||||||
|
|
||||||
title: Ember.computed.alias('controllers.topic.model.title'),
|
title: Ember.computed.alias('controllers.topic.model.title'),
|
||||||
|
canReplyAsNewTopic: Ember.computed.alias('controllers.topic.model.details.can_reply_as_new_topic'),
|
||||||
|
|
||||||
@computed('type', 'postNumber')
|
@computed('type', 'postNumber')
|
||||||
shareTitle(type, postNumber) {
|
shareTitle(type, postNumber) {
|
||||||
|
@ -29,6 +30,15 @@ export default Ember.Controller.extend({
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
replyAsNewTopic() {
|
||||||
|
const topicController = this.get("controllers.topic");
|
||||||
|
const postStream = topicController.get("model.postStream");
|
||||||
|
const postId = postStream.findPostIdForPostNumber(this.get("postNumber"));
|
||||||
|
const post = postStream.findLoadedPost(postId);
|
||||||
|
topicController.send("replyAsNewTopic", post);
|
||||||
|
this.send("close");
|
||||||
|
},
|
||||||
|
|
||||||
share(source) {
|
share(source) {
|
||||||
var url = source.generateUrl(this.get('link'), this.get('title'));
|
var url = source.generateUrl(this.get('link'), this.get('title'));
|
||||||
if (source.shouldOpenInPopup) {
|
if (source.shouldOpenInPopup) {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
needs: ['tagGroups'],
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
save() {
|
||||||
|
this.get('model').save();
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
const self = this;
|
||||||
|
return bootbox.confirm(
|
||||||
|
I18n.t("tagging.groups.confirm_delete"),
|
||||||
|
I18n.t("no_value"),
|
||||||
|
I18n.t("yes_value"),
|
||||||
|
function(destroy) {
|
||||||
|
if (destroy) {
|
||||||
|
const c = self.controllerFor('tagGroups');
|
||||||
|
return self.get('model').destroy().then(function() {
|
||||||
|
c.removeObject(self.get('model'));
|
||||||
|
self.transitionToRoute('tagGroups');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
export default Ember.ArrayController.extend({
|
||||||
|
actions: {
|
||||||
|
selectTagGroup: function(tagGroup) {
|
||||||
|
if (this.get('selectedItem')) { this.get('selectedItem').set('selected', false); }
|
||||||
|
this.set('selectedItem', tagGroup);
|
||||||
|
tagGroup.set('selected', true);
|
||||||
|
tagGroup.set('savingStatus', null);
|
||||||
|
this.transitionToRoute('tagGroups.show', tagGroup);
|
||||||
|
},
|
||||||
|
|
||||||
|
newTagGroup: function() {
|
||||||
|
const newTagGroup = this.store.createRecord('tag-group');
|
||||||
|
newTagGroup.set('name', I18n.t('tagging.groups.new_name'));
|
||||||
|
this.pushObject(newTagGroup);
|
||||||
|
this.send('selectTagGroup', newTagGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
sortProperties: ['count:desc', 'id'],
|
sortProperties: ['count:desc', 'id'],
|
||||||
|
|
||||||
sortedTags: Ember.computed.sort('model', 'sortProperties'),
|
canAdminTags: Ember.computed.alias("currentUser.staff"),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
sortByCount() {
|
sortByCount() {
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
import DiscourseURL from 'discourse/lib/url';
|
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
|
||||||
needs: ['topic'],
|
|
||||||
progressPosition: null,
|
|
||||||
expanded: false,
|
|
||||||
toPostIndex: null,
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
toggleExpansion(opts) {
|
|
||||||
this.toggleProperty('expanded');
|
|
||||||
if (this.get('expanded')) {
|
|
||||||
this.set('toPostIndex', this.get('progressPosition'));
|
|
||||||
if(opts && opts.highlight){
|
|
||||||
// TODO: somehow move to view?
|
|
||||||
Em.run.next(function(){
|
|
||||||
$('.jump-form input').select().focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
jumpPost() {
|
|
||||||
var postIndex = parseInt(this.get('toPostIndex'), 10);
|
|
||||||
|
|
||||||
// Validate the post index first
|
|
||||||
if (isNaN(postIndex) || postIndex < 1) {
|
|
||||||
postIndex = 1;
|
|
||||||
}
|
|
||||||
if (postIndex > this.get('model.postStream.filteredPostsCount')) {
|
|
||||||
postIndex = this.get('model.postStream.filteredPostsCount');
|
|
||||||
}
|
|
||||||
this.set('toPostIndex', postIndex);
|
|
||||||
var stream = this.get('model.postStream'),
|
|
||||||
postId = stream.findPostIdForPostNumber(postIndex);
|
|
||||||
|
|
||||||
if (!postId) {
|
|
||||||
Em.Logger.warn("jump-post code broken - requested an index outside the stream array");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var post = stream.findLoadedPost(postId);
|
|
||||||
if (post) {
|
|
||||||
this.jumpTo(this.get('model').urlForPostNumber(post.get('post_number')));
|
|
||||||
} else {
|
|
||||||
var self = this;
|
|
||||||
// need to load it
|
|
||||||
stream.findPostsByIds([postId]).then(function(arr) {
|
|
||||||
post = arr[0];
|
|
||||||
self.jumpTo(self.get('model').urlForPostNumber(post.get('post_number')));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
jumpTop() {
|
|
||||||
this.jumpTo(this.get('model.firstPostUrl'));
|
|
||||||
},
|
|
||||||
|
|
||||||
jumpBottom() {
|
|
||||||
this.jumpTo(this.get('model.lastPostUrl'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Route and close the expansion
|
|
||||||
jumpTo(url) {
|
|
||||||
this.set('expanded', false);
|
|
||||||
DiscourseURL.routeTo(url);
|
|
||||||
},
|
|
||||||
|
|
||||||
streamPercentage: function() {
|
|
||||||
if (!this.get('model.postStream.loaded')) { return 0; }
|
|
||||||
if (this.get('model.postStream.highest_post_number') === 0) { return 0; }
|
|
||||||
var perc = this.get('progressPosition') / this.get('model.postStream.filteredPostsCount');
|
|
||||||
return (perc > 1.0) ? 1.0 : perc;
|
|
||||||
}.property('model.postStream.loaded', 'progressPosition', 'model.postStream.filteredPostsCount'),
|
|
||||||
|
|
||||||
jumpTopDisabled: function() {
|
|
||||||
return this.get('progressPosition') <= 3;
|
|
||||||
}.property('progressPosition'),
|
|
||||||
|
|
||||||
filteredPostCountChanged: function(){
|
|
||||||
if(this.get('model.postStream.filteredPostsCount') < this.get('progressPosition')){
|
|
||||||
this.set('progressPosition', this.get('model.postStream.filteredPostsCount'));
|
|
||||||
}
|
|
||||||
}.observes('model.postStream.filteredPostsCount'),
|
|
||||||
|
|
||||||
jumpBottomDisabled: function() {
|
|
||||||
return this.get('progressPosition') >= this.get('model.postStream.filteredPostsCount') ||
|
|
||||||
this.get('progressPosition') >= this.get('model.highest_post_number');
|
|
||||||
}.property('model.postStream.filteredPostsCount', 'model.highest_post_number', 'progressPosition'),
|
|
||||||
|
|
||||||
hideProgress: function() {
|
|
||||||
if (!this.get('model.postStream.loaded')) return true;
|
|
||||||
if (!this.get('model.currentPost')) return true;
|
|
||||||
if (this.get('model.postStream.filteredPostsCount') < 2) return true;
|
|
||||||
return false;
|
|
||||||
}.property('model.postStream.loaded', 'model.currentPost', 'model.postStream.filteredPostsCount'),
|
|
||||||
|
|
||||||
hugeNumberOfPosts: function() {
|
|
||||||
return (this.get('model.postStream.filteredPostsCount') >= Discourse.SiteSettings.short_progress_text_threshold);
|
|
||||||
}.property('model.highest_post_number'),
|
|
||||||
|
|
||||||
jumpToBottomTitle: function() {
|
|
||||||
if (this.get('hugeNumberOfPosts')) {
|
|
||||||
return I18n.t('topic.progress.jump_bottom_with_number', {post_number: this.get('model.highest_post_number')});
|
|
||||||
} else {
|
|
||||||
return I18n.t('topic.progress.jump_bottom');
|
|
||||||
}
|
|
||||||
}.property('hugeNumberOfPosts', 'model.highest_post_number')
|
|
||||||
|
|
||||||
});
|
|
|
@ -10,20 +10,38 @@ import DiscourseURL from 'discourse/lib/url';
|
||||||
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
||||||
|
|
||||||
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
needs: ['modal', 'composer', 'quote-button', 'topic-progress', 'application'],
|
needs: ['modal', 'composer', 'quote-button', 'application'],
|
||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
allPostsSelected: false,
|
allPostsSelected: false,
|
||||||
editingTopic: false,
|
editingTopic: false,
|
||||||
selectedPosts: null,
|
selectedPosts: null,
|
||||||
selectedReplies: null,
|
selectedReplies: null,
|
||||||
queryParams: ['filter', 'username_filters', 'show_deleted'],
|
queryParams: ['filter', 'username_filters', 'show_deleted'],
|
||||||
loadedAllPosts: Em.computed.or('model.postStream.loadedAllPosts', 'model.postStream.loadingLastPost'),
|
loadedAllPosts: Ember.computed.or('model.postStream.loadedAllPosts', 'model.postStream.loadingLastPost'),
|
||||||
enteredAt: null,
|
enteredAt: null,
|
||||||
|
enteredIndex: null,
|
||||||
retrying: false,
|
retrying: false,
|
||||||
adminMenuVisible: false,
|
userTriggeredProgress: null,
|
||||||
|
_progressIndex: null,
|
||||||
|
|
||||||
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
|
topicDelegated: [
|
||||||
isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"),
|
'toggleMultiSelect',
|
||||||
|
'deleteTopic',
|
||||||
|
'recoverTopic',
|
||||||
|
'toggleClosed',
|
||||||
|
'showAutoClose',
|
||||||
|
'showFeatureTopic',
|
||||||
|
'showChangeTimestamp',
|
||||||
|
'toggleArchived',
|
||||||
|
'toggleVisibility',
|
||||||
|
'convertToPublicTopic',
|
||||||
|
'convertToPrivateMessage',
|
||||||
|
'jumpTop',
|
||||||
|
'jumpToPost',
|
||||||
|
'jumpToIndex',
|
||||||
|
'jumpBottom',
|
||||||
|
'replyToPost'
|
||||||
|
],
|
||||||
|
|
||||||
_titleChanged: function() {
|
_titleChanged: function() {
|
||||||
const title = this.get('model.title');
|
const title = this.get('model.title');
|
||||||
|
@ -176,19 +194,39 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
return this.get('model.postStream').fillGapAfter(args.post, args.gap);
|
return this.get('model.postStream').fillGapAfter(args.post, args.gap);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
currentPostChanged(event) {
|
||||||
|
const { post } = event;
|
||||||
|
if (!post) { return; }
|
||||||
|
|
||||||
|
const postNumber = post.get('post_number');
|
||||||
|
const topic = this.get('model');
|
||||||
|
topic.set('currentPost', postNumber);
|
||||||
|
if (postNumber > (topic.get('last_read_post_number') || 0)) {
|
||||||
|
topic.set('last_read_post_id', post.get('id'));
|
||||||
|
topic.set('last_read_post_number', postNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.send('postChangedRoute', postNumber);
|
||||||
|
this._progressIndex = topic.get('postStream').progressIndexOfPost(post);
|
||||||
|
},
|
||||||
|
|
||||||
|
currentPostScrolled(event) {
|
||||||
|
const total = this.get('model.postStream.filteredPostsCount');
|
||||||
|
const percent = (parseFloat(this._progressIndex + event.percent - 1) / total);
|
||||||
|
this.appEvents.trigger('topic:current-post-scrolled', {
|
||||||
|
postIndex: this._progressIndex,
|
||||||
|
percent: Math.max(Math.min(percent, 1.0), 0.0)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Called the the topmost visible post on the page changes.
|
// Called the the topmost visible post on the page changes.
|
||||||
topVisibleChanged(event) {
|
topVisibleChanged(event) {
|
||||||
const { post, refresh } = event;
|
const { post, refresh } = event;
|
||||||
|
|
||||||
if (!post) { return; }
|
if (!post) { return; }
|
||||||
|
|
||||||
const postStream = this.get('model.postStream');
|
const postStream = this.get('model.postStream');
|
||||||
const firstLoadedPost = postStream.get('posts.firstObject');
|
const firstLoadedPost = postStream.get('posts.firstObject');
|
||||||
|
|
||||||
const currentPostNumber = post.get('post_number');
|
|
||||||
this.set('model.currentPost', currentPostNumber);
|
|
||||||
this.send('postChangedRoute', currentPostNumber);
|
|
||||||
|
|
||||||
if (post.get('post_number') === 1) { return; }
|
if (post.get('post_number') === 1) { return; }
|
||||||
|
|
||||||
if (firstLoadedPost && firstLoadedPost === post) {
|
if (firstLoadedPost && firstLoadedPost === post) {
|
||||||
|
@ -196,15 +234,13 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Called the the bottommost visible post on the page changes.
|
// Called the the bottommost visible post on the page changes.
|
||||||
bottomVisibleChanged(event) {
|
bottomVisibleChanged(event) {
|
||||||
const { post, refresh } = event;
|
const { post, refresh } = event;
|
||||||
|
|
||||||
const postStream = this.get('model.postStream');
|
const postStream = this.get('model.postStream');
|
||||||
const lastLoadedPost = postStream.get('posts.lastObject');
|
const lastLoadedPost = postStream.get('posts.lastObject');
|
||||||
|
|
||||||
this.set('controllers.topic-progress.progressPosition', postStream.progressIndexOfPost(post));
|
|
||||||
|
|
||||||
if (lastLoadedPost && lastLoadedPost === post && postStream.get('canAppendMore')) {
|
if (lastLoadedPost && lastLoadedPost === post && postStream.get('canAppendMore')) {
|
||||||
postStream.appendMore().then(() => refresh());
|
postStream.appendMore().then(() => refresh());
|
||||||
// show loading stuff
|
// show loading stuff
|
||||||
|
@ -220,14 +256,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
return this.get('model.details').removeAllowedUser(user);
|
return this.get('model.details').removeAllowedUser(user);
|
||||||
},
|
},
|
||||||
|
|
||||||
showTopicAdminMenu() {
|
|
||||||
this.set('adminMenuVisible', true);
|
|
||||||
},
|
|
||||||
|
|
||||||
hideTopicAdminMenu() {
|
|
||||||
this.set('adminMenuVisible', false);
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteTopic() {
|
deleteTopic() {
|
||||||
this.deleteTopic();
|
this.deleteTopic();
|
||||||
},
|
},
|
||||||
|
@ -378,8 +406,20 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
jumpToIndex(index) {
|
||||||
|
this._jumpToPostId(this.get('model.postStream.stream')[index-1]);
|
||||||
|
},
|
||||||
|
|
||||||
|
jumpToPost(postNumber) {
|
||||||
|
this._jumpToPostId(this.get('model.postStream').findPostIdForPostNumber(postNumber));
|
||||||
|
},
|
||||||
|
|
||||||
jumpTop() {
|
jumpTop() {
|
||||||
this.get('controllers.topic-progress').send('jumpTop');
|
DiscourseURL.routeTo(this.get('model.firstPostUrl'), { skipIfOnScreen: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
jumpBottom() {
|
||||||
|
DiscourseURL.routeTo(this.get('model.lastPostUrl'), { skipIfOnScreen: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
selectAll() {
|
selectAll() {
|
||||||
|
@ -546,10 +586,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
},
|
},
|
||||||
|
|
||||||
replyAsNewTopic(post) {
|
replyAsNewTopic(post) {
|
||||||
const composerController = this.get('controllers.composer'),
|
const composerController = this.get('controllers.composer');
|
||||||
quoteController = this.get('controllers.quote-button'),
|
const quoteController = this.get('controllers.quote-button');
|
||||||
quotedText = Quote.build(quoteController.get('post'), quoteController.get('buffer')),
|
post = post || quoteController.get('post');
|
||||||
self = this;
|
const quotedText = Quote.build(post, quoteController.get('buffer'));
|
||||||
|
|
||||||
quoteController.deselectText();
|
quoteController.deselectText();
|
||||||
|
|
||||||
|
@ -561,7 +601,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
return Em.isEmpty(quotedText) ? "" : quotedText;
|
return Em.isEmpty(quotedText) ? "" : quotedText;
|
||||||
}).then(q => {
|
}).then(q => {
|
||||||
const postUrl = `${location.protocol}//${location.host}${post.get('url')}`;
|
const postUrl = `${location.protocol}//${location.host}${post.get('url')}`;
|
||||||
const postLink = `[${Handlebars.escapeExpression(self.get('model.title'))}](${postUrl})`;
|
const postLink = `[${Handlebars.escapeExpression(this.get('model.title'))}](${postUrl})`;
|
||||||
composerController.get('model').prependText(`${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`, {new_line: true});
|
composerController.get('model').prependText(`${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`, {new_line: true});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -609,6 +649,25 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_jumpToPostId(postId) {
|
||||||
|
if (!postId) {
|
||||||
|
Ember.Logger.warn("jump-post code broken - requested an index outside the stream array");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const topic = this.get('model');
|
||||||
|
const postStream = topic.get('postStream');
|
||||||
|
const post = postStream.findLoadedPost(postId);
|
||||||
|
if (post) {
|
||||||
|
DiscourseURL.routeTo(topic.urlForPostNumber(post.get('post_number')));
|
||||||
|
} else {
|
||||||
|
// need to load it
|
||||||
|
postStream.findPostsByIds([postId]).then(arr => {
|
||||||
|
DiscourseURL.routeTo(topic.urlForPostNumber(arr[0].get('post_number')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
togglePinnedState() {
|
togglePinnedState() {
|
||||||
this.send('togglePinnedForUser');
|
this.send('togglePinnedForUser');
|
||||||
},
|
},
|
||||||
|
@ -792,27 +851,23 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
const postStream = topic.get("postStream");
|
const postStream = topic.get("postStream");
|
||||||
|
|
||||||
if (topic.get('id') === topicId) {
|
if (topic.get('id') === topicId) {
|
||||||
|
|
||||||
// TODO identity map for postNumber
|
|
||||||
postStream.get('posts').forEach(post => {
|
postStream.get('posts').forEach(post => {
|
||||||
if (!post.read && postNumbers.indexOf(post.post_number) !== -1) {
|
if (!post.read && postNumbers.indexOf(post.post_number) !== -1) {
|
||||||
post.set('read', true);
|
post.set('read', true);
|
||||||
this.appEvents.trigger('post-stream:refresh', { id: post.id });
|
this.appEvents.trigger('post-stream:refresh', { id: post.get('id') });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const max = _.max(postNumbers);
|
|
||||||
if (max > topic.get("last_read_post_number")) {
|
|
||||||
topic.set("last_read_post_number", max);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.siteSettings.automatically_unpin_topics &&
|
if (this.siteSettings.automatically_unpin_topics &&
|
||||||
this.currentUser &&
|
this.currentUser &&
|
||||||
this.currentUser.automatically_unpin_topics) {
|
this.currentUser.automatically_unpin_topics) {
|
||||||
|
|
||||||
// automatically unpin topics when the user reaches the bottom
|
// automatically unpin topics when the user reaches the bottom
|
||||||
|
const max = _.max(postNumbers);
|
||||||
if (topic.get("pinned") && max >= topic.get("highest_post_number")) {
|
if (topic.get("pinned") && max >= topic.get("highest_post_number")) {
|
||||||
Em.run.next(() => topic.clearPin());
|
Em.run.next(() => topic.clearPin());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,6 +29,11 @@ export default Ember.Controller.extend({
|
||||||
linkWebsite: Em.computed.not('user.isBasic'),
|
linkWebsite: Em.computed.not('user.isBasic'),
|
||||||
hasLocationOrWebsite: Em.computed.or('user.location', 'user.website_name'),
|
hasLocationOrWebsite: Em.computed.or('user.location', 'user.website_name'),
|
||||||
|
|
||||||
|
@computed('user.name')
|
||||||
|
nameFirst(name) {
|
||||||
|
return !this.get('siteSettings.prioritize_username_in_ux') && name && name.trim().length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
@computed('user.user_fields.@each.value')
|
@computed('user.user_fields.@each.value')
|
||||||
publicUserFields() {
|
publicUserFields() {
|
||||||
const siteUserFields = this.site.get('user_fields');
|
const siteUserFields = this.site.get('user_fields');
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Invite from 'discourse/models/invite';
|
import Invite from 'discourse/models/invite';
|
||||||
import debounce from 'discourse/lib/debounce';
|
import debounce from 'discourse/lib/debounce';
|
||||||
|
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||||
|
|
||||||
// This controller handles actions related to a user's invitations
|
// This controller handles actions related to a user's invitations
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
|
@ -10,6 +11,7 @@ export default Ember.Controller.extend({
|
||||||
invitesCount: null,
|
invitesCount: null,
|
||||||
canLoadMore: true,
|
canLoadMore: true,
|
||||||
invitesLoading: false,
|
invitesLoading: false,
|
||||||
|
reinvitedAll: false,
|
||||||
|
|
||||||
init: function() {
|
init: function() {
|
||||||
this._super();
|
this._super();
|
||||||
|
@ -32,6 +34,10 @@ export default Ember.Controller.extend({
|
||||||
|
|
||||||
inviteRedeemed: Em.computed.equal('filter', 'redeemed'),
|
inviteRedeemed: Em.computed.equal('filter', 'redeemed'),
|
||||||
|
|
||||||
|
showReinviteAllButton: function() {
|
||||||
|
return (this.get('filter') === "pending" && this.get('model').invites.length > 4 && this.currentUser.get('staff'));
|
||||||
|
}.property('filter'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Can the currently logged in user invite users to the site
|
Can the currently logged in user invite users to the site
|
||||||
|
|
||||||
|
@ -87,6 +93,13 @@ export default Ember.Controller.extend({
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reinviteAll() {
|
||||||
|
const self = this;
|
||||||
|
Invite.reinviteAll().then(function() {
|
||||||
|
self.set('reinvitedAll', true);
|
||||||
|
}).catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
|
||||||
loadMore() {
|
loadMore() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var model = self.get('model');
|
var model = self.get('model');
|
||||||
|
|
|
@ -41,7 +41,12 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||||
return viewingSelf || staff;
|
return viewingSelf || staff;
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("content.badge_count")
|
@computed('model.name')
|
||||||
|
nameFirst(name) {
|
||||||
|
return !this.get('siteSettings.prioritize_username_in_ux') && name && name.trim().length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("model.badge_count")
|
||||||
showBadges(badgeCount) {
|
showBadges(badgeCount) {
|
||||||
return Discourse.SiteSettings.enable_badges && badgeCount > 0;
|
return Discourse.SiteSettings.enable_badges && badgeCount > 0;
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
export default {
|
||||||
|
name: "auth-complete",
|
||||||
|
after: "inject-objects",
|
||||||
|
initialize() {
|
||||||
|
if (window.location.search.indexOf('authComplete=true') !== -1) {
|
||||||
|
const lastAuthResult = localStorage.getItem('lastAuthResult');
|
||||||
|
if (lastAuthResult) {
|
||||||
|
Ember.run.next(() => Discourse.authenticationComplete(JSON.parse(lastAuthResult)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import loadScript from 'discourse/lib/load-script';
|
import loadScript from 'discourse/lib/load-script';
|
||||||
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
|
|
||||||
// Use the message bus for live reloading of components for faster development.
|
// Use the message bus for live reloading of components for faster development.
|
||||||
export default {
|
export default {
|
||||||
|
@ -30,6 +31,11 @@ export default {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Useful to export this for debugging purposes
|
||||||
|
if (Discourse.Environment === 'development' && !Ember.testing) {
|
||||||
|
window.DiscourseURL = DiscourseURL;
|
||||||
|
}
|
||||||
|
|
||||||
// Observe file changes
|
// Observe file changes
|
||||||
messageBus.subscribe("/file-change", function(data) {
|
messageBus.subscribe("/file-change", function(data) {
|
||||||
if (Handlebars.compile && !Ember.TEMPLATES.empty) {
|
if (Handlebars.compile && !Ember.TEMPLATES.empty) {
|
||||||
|
|
|
@ -55,7 +55,12 @@ function shortDateNoYear(date) {
|
||||||
return moment(date).format(I18n.t("dates.tiny.date_month"));
|
return moment(date).format(I18n.t("dates.tiny.date_month"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function tinyDateYear(date) {
|
// Suppress year if it's this year
|
||||||
|
export function smartShortDate(date, withYear=tinyDateYear) {
|
||||||
|
return (date.getFullYear() === new Date().getFullYear()) ? shortDateNoYear(date) : withYear(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tinyDateYear(date) {
|
||||||
return moment(date).format(I18n.t("dates.tiny.date_year"));
|
return moment(date).format(I18n.t("dates.tiny.date_year"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,47 +125,46 @@ export function autoUpdatingRelativeAge(date,options) {
|
||||||
return "<span class='relative-date" + append + "' data-time='" + date.getTime() + "' data-format='" + format + "'>" + relAge + "</span>";
|
return "<span class='relative-date" + append + "' data-time='" + date.getTime() + "' data-format='" + format + "'>" + relAge + "</span>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrapAgo(dateStr) {
|
||||||
|
return I18n.t("dates.wrap_ago", { date: dateStr });
|
||||||
|
}
|
||||||
|
|
||||||
function relativeAgeTiny(date){
|
function relativeAgeTiny(date, ageOpts) {
|
||||||
const format = "tiny";
|
const format = "tiny";
|
||||||
const distance = Math.round((new Date() - date) / 1000);
|
const distance = Math.round((new Date() - date) / 1000);
|
||||||
const distanceInMinutes = Math.round(distance / 60.0);
|
const distanceInMinutes = Math.round(distance / 60.0);
|
||||||
|
|
||||||
let formatted;
|
let formatted;
|
||||||
const t = function(key,opts){
|
const t = function(key, opts) {
|
||||||
return I18n.t("dates." + format + "." + key, opts);
|
const result = I18n.t("dates." + format + "." + key, opts);
|
||||||
|
return (ageOpts && ageOpts.addAgo) ? wrapAgo(result) : result;
|
||||||
};
|
};
|
||||||
|
|
||||||
switch(true){
|
switch(true) {
|
||||||
|
case(distanceInMinutes < 1):
|
||||||
case(distanceInMinutes < 1):
|
formatted = t("less_than_x_minutes", {count: 1});
|
||||||
formatted = t("less_than_x_minutes", {count: 1});
|
break;
|
||||||
break;
|
case(distanceInMinutes >= 1 && distanceInMinutes <= 44):
|
||||||
case(distanceInMinutes >= 1 && distanceInMinutes <= 44):
|
formatted = t("x_minutes", {count: distanceInMinutes});
|
||||||
formatted = t("x_minutes", {count: distanceInMinutes});
|
break;
|
||||||
break;
|
case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
|
||||||
case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
|
formatted = t("about_x_hours", {count: 1});
|
||||||
formatted = t("about_x_hours", {count: 1});
|
break;
|
||||||
break;
|
case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
|
||||||
case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
|
formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
|
||||||
formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
|
break;
|
||||||
break;
|
case(Discourse.SiteSettings.relative_date_duration === 0 && distanceInMinutes <= 525599):
|
||||||
case(Discourse.SiteSettings.relative_date_duration === 0 && distanceInMinutes <= 525599):
|
|
||||||
formatted = shortDateNoYear(date);
|
|
||||||
break;
|
|
||||||
case(distanceInMinutes >= 1410 && distanceInMinutes <= 2519):
|
|
||||||
formatted = t("x_days", {count: 1});
|
|
||||||
break;
|
|
||||||
case(distanceInMinutes >= 2520 && distanceInMinutes <= ((Discourse.SiteSettings.relative_date_duration||14) * 1440)):
|
|
||||||
formatted = t("x_days", {count: Math.round(distanceInMinutes / 1440.0)});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if(date.getFullYear() === new Date().getFullYear()) {
|
|
||||||
formatted = shortDateNoYear(date);
|
formatted = shortDateNoYear(date);
|
||||||
} else {
|
break;
|
||||||
formatted = tinyDateYear(date);
|
case(distanceInMinutes >= 1410 && distanceInMinutes <= 2519):
|
||||||
}
|
formatted = t("x_days", {count: 1});
|
||||||
break;
|
break;
|
||||||
|
case(distanceInMinutes >= 2520 && distanceInMinutes <= ((Discourse.SiteSettings.relative_date_duration||14) * 1440)):
|
||||||
|
formatted = t("x_days", {count: Math.round(distanceInMinutes / 1440.0)});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
formatted = (ageOpts.defaultFormat || smartShortDate)(date);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatted;
|
return formatted;
|
||||||
|
@ -199,7 +203,7 @@ function relativeAgeMediumSpan(distance, leaveAgo) {
|
||||||
formatted = t("x_days", {count: Math.round((distanceInMinutes - 720.0) / 1440.0)});
|
formatted = t("x_days", {count: Math.round((distanceInMinutes - 720.0) / 1440.0)});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return formatted || '&mdash';
|
return formatted || '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
function relativeAgeMedium(date, options) {
|
function relativeAgeMedium(date, options) {
|
||||||
|
@ -219,11 +223,7 @@ function relativeAgeMedium(date, options) {
|
||||||
if (distance < oneMinuteAgo) {
|
if (distance < oneMinuteAgo) {
|
||||||
displayDate = I18n.t("now");
|
displayDate = I18n.t("now");
|
||||||
} else if (distance > fiveDaysAgo) {
|
} else if (distance > fiveDaysAgo) {
|
||||||
if ((new Date()).getFullYear() !== date.getFullYear()) {
|
displayDate = smartShortDate(date, shortDate);
|
||||||
displayDate = shortDate(date);
|
|
||||||
} else {
|
|
||||||
displayDate = shortDateNoYear(date);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
displayDate = relativeAgeMediumSpan(distance, leaveAgo);
|
displayDate = relativeAgeMediumSpan(distance, leaveAgo);
|
||||||
}
|
}
|
||||||
|
@ -239,7 +239,7 @@ export function relativeAge(date, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
const format = options.format || "tiny";
|
const format = options.format || "tiny";
|
||||||
|
|
||||||
if(format === "tiny") {
|
if (format === "tiny") {
|
||||||
return relativeAgeTiny(date, options);
|
return relativeAgeTiny(date, options);
|
||||||
} else if (format === "medium") {
|
} else if (format === "medium") {
|
||||||
return relativeAgeMedium(date, options);
|
return relativeAgeMedium(date, options);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import DiscourseURL from 'discourse/lib/url';
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
import Composer from 'discourse/models/composer';
|
import Composer from 'discourse/models/composer';
|
||||||
|
import { scrollTopFor } from 'discourse/lib/offset-calculator';
|
||||||
|
|
||||||
const bindings = {
|
const bindings = {
|
||||||
'!': {postAction: 'showFlags'},
|
'!': {postAction: 'showFlags'},
|
||||||
|
@ -116,7 +117,7 @@ export default {
|
||||||
|
|
||||||
_jumpTo(direction) {
|
_jumpTo(direction) {
|
||||||
if ($('.container.posts').length) {
|
if ($('.container.posts').length) {
|
||||||
this.container.lookup('controller:topic-progress').send(direction);
|
this.container.lookup('controller:topic').send(direction);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -159,7 +160,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleProgress() {
|
toggleProgress() {
|
||||||
this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true});
|
this.appEvents.trigger('topic-progress:keyboard-trigger', { type: 'jump' });
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleSearch(event) {
|
toggleSearch(event) {
|
||||||
|
@ -298,12 +299,19 @@ export default {
|
||||||
|
|
||||||
if ($article.is('.topic-post')) {
|
if ($article.is('.topic-post')) {
|
||||||
$('a.tabLoc', $article).focus();
|
$('a.tabLoc', $article).focus();
|
||||||
}
|
this._scrollToPost($article);
|
||||||
|
|
||||||
this._scrollList($article, direction);
|
} else {
|
||||||
|
this._scrollList($article, direction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_scrollToPost($article) {
|
||||||
|
const pos = $article.offset();
|
||||||
|
$(window).scrollTop(Math.ceil(pos.top - scrollTopFor(pos.top)));
|
||||||
|
},
|
||||||
|
|
||||||
_scrollList($article) {
|
_scrollList($article) {
|
||||||
// Try to keep the article on screen
|
// Try to keep the article on screen
|
||||||
const pos = $article.offset();
|
const pos = $article.offset();
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
const _warned = {};
|
||||||
|
|
||||||
|
const NO_RESULT = [false, null];
|
||||||
|
|
||||||
|
export default class LinkLookup {
|
||||||
|
|
||||||
|
constructor(links) {
|
||||||
|
this._links = links;
|
||||||
|
}
|
||||||
|
|
||||||
|
check(post, href) {
|
||||||
|
if (_warned[href]) { return NO_RESULT; }
|
||||||
|
|
||||||
|
const normalized = href.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||||
|
if (_warned[normalized]) { return NO_RESULT; }
|
||||||
|
|
||||||
|
const linkInfo = this._links[normalized];
|
||||||
|
if (linkInfo) {
|
||||||
|
// Skip edits to the same post
|
||||||
|
if (post && post.get('post_number') === linkInfo.post_number) { return NO_RESULT; }
|
||||||
|
|
||||||
|
_warned[href] = true;
|
||||||
|
_warned[normalized] = true;
|
||||||
|
return [true, linkInfo];
|
||||||
|
}
|
||||||
|
|
||||||
|
return NO_RESULT;
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { scrollTopFor } from 'discourse/lib/offset-calculator';
|
||||||
|
|
||||||
// Dear traveller, you are entering a zone where we are at war with the browser
|
// Dear traveller, you are entering a zone where we are at war with the browser
|
||||||
// the browser is insisting on positioning scrollTop per the location it was in
|
// the browser is insisting on positioning scrollTop per the location it was in
|
||||||
// the past, we are insisting on it being where we want it to be
|
// the past, we are insisting on it being where we want it to be
|
||||||
|
@ -16,75 +18,66 @@
|
||||||
// 1. onbeforeunload ensure we are scrolled to the right spot
|
// 1. onbeforeunload ensure we are scrolled to the right spot
|
||||||
// 2. give up on the scrollbar and implement it ourselves (something that will happen)
|
// 2. give up on the scrollbar and implement it ourselves (something that will happen)
|
||||||
|
|
||||||
(function (exports) {
|
const SCROLL_EVENTS = "scroll.lock-on touchmove.lock-on mousedown.lock-on wheel.lock-on DOMMouseScroll.lock-on mousewheel.lock-on keyup.lock-on";
|
||||||
|
|
||||||
var scrollEvents = "scroll.lock-on touchmove.lock-on mousedown.lock-on wheel.lock-on DOMMouseScroll.lock-on mousewheel.lock-on keyup.lock-on";
|
function within(threshold, x, y) {
|
||||||
|
return Math.abs(x-y) < threshold;
|
||||||
|
}
|
||||||
|
|
||||||
var LockOn = function(selector, options) {
|
export default class LockOn {
|
||||||
|
constructor(selector, options) {
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
this.options = options || {};
|
this.options = options || {};
|
||||||
};
|
this.offsetTop = null;
|
||||||
|
}
|
||||||
LockOn.prototype.elementTop = function() {
|
|
||||||
var offsetCalculator = this.options.offsetCalculator,
|
|
||||||
selected = $(this.selector);
|
|
||||||
|
|
||||||
|
elementTop() {
|
||||||
|
const selected = $(this.selector);
|
||||||
if (selected && selected.offset && selected.offset()) {
|
if (selected && selected.offset && selected.offset()) {
|
||||||
return selected.offset().top - (offsetCalculator ? offsetCalculator() : 0);
|
const result = selected.offset().top;
|
||||||
|
return result - Math.round(scrollTopFor(result));
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
LockOn.prototype.lock = function() {
|
clearLock(interval) {
|
||||||
var self = this,
|
$('body,html').off(SCROLL_EVENTS);
|
||||||
previousTop = this.elementTop(),
|
clearInterval(interval);
|
||||||
startedAt = new Date().getTime(),
|
}
|
||||||
i = 0;
|
|
||||||
|
lock() {
|
||||||
|
let previousTop = this.elementTop();
|
||||||
|
const startedAt = new Date().getTime();
|
||||||
|
|
||||||
$(window).scrollTop(previousTop);
|
$(window).scrollTop(previousTop);
|
||||||
|
|
||||||
var within = function(threshold,x,y) {
|
let i = 0;
|
||||||
return Math.abs(x-y) < threshold;
|
|
||||||
};
|
|
||||||
|
|
||||||
var interval = setInterval(function() {
|
const interval = setInterval(() => {
|
||||||
i = i + 1;
|
i = i + 1;
|
||||||
|
|
||||||
var top = self.elementTop(),
|
let top = this.elementTop();
|
||||||
scrollTop = $(window).scrollTop();
|
const scrollTop = $(window).scrollTop();
|
||||||
|
|
||||||
if (typeof(top) === "undefined") {
|
if (typeof(top) === "undefined" || isNaN(top)) {
|
||||||
$('body,html').off(scrollEvents);
|
return this.clearLock(interval);
|
||||||
clearInterval(interval);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!within(4, top, previousTop) || !within(4, scrollTop, top)) {
|
if (!within(4, top, previousTop) || !within(4, scrollTop, top)) {
|
||||||
$(window).scrollTop(top);
|
$(window).scrollTop(top);
|
||||||
// animating = true;
|
|
||||||
// $('html,body').animate({scrollTop: parseInt(top,10)+'px'}, 200, 'swing', function(){
|
|
||||||
// animating = false;
|
|
||||||
// });
|
|
||||||
previousTop = top;
|
previousTop = top;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We commit suicide after 3s just to clean up
|
// We commit suicide after 3s just to clean up
|
||||||
var nowTime = new Date().getTime();
|
const nowTime = new Date().getTime();
|
||||||
if (nowTime - startedAt > 1000) {
|
if (nowTime - startedAt > 1000) {
|
||||||
$('body,html').off(scrollEvents);
|
return this.clearLock(interval);
|
||||||
clearInterval(interval);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
$('body,html').off(scrollEvents).on(scrollEvents, function(e){
|
$('body,html').off(SCROLL_EVENTS).on(SCROLL_EVENTS, e => {
|
||||||
if ( e.which > 0 || e.type === "mousedown" || e.type === "mousewheel" || e.type === "touchmove") {
|
if ( e.which > 0 || e.type === "mousedown" || e.type === "mousewheel" || e.type === "touchmove") {
|
||||||
$('body,html').off(scrollEvents);
|
this.clearLock(interval);
|
||||||
clearInterval(interval);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
exports.LockOn = LockOn;
|
|
||||||
|
|
||||||
})(window);
|
|
|
@ -1,6 +1,24 @@
|
||||||
export default {
|
const NotificationLevels = {
|
||||||
WATCHING: 3,
|
WATCHING: 3,
|
||||||
TRACKING: 2,
|
TRACKING: 2,
|
||||||
REGULAR: 1,
|
REGULAR: 1,
|
||||||
MUTED: 0
|
MUTED: 0
|
||||||
};
|
};
|
||||||
|
export default NotificationLevels;
|
||||||
|
|
||||||
|
export function buttonDetails(level) {
|
||||||
|
switch(level) {
|
||||||
|
case NotificationLevels.WATCHING:
|
||||||
|
return { id: NotificationLevels.WATCHING, key: 'watching', icon: 'exclamation-circle' };
|
||||||
|
case NotificationLevels.TRACKING:
|
||||||
|
return { id: NotificationLevels.TRACKING, key: 'tracking', icon: 'circle' };
|
||||||
|
case NotificationLevels.MUTED:
|
||||||
|
return { id: NotificationLevels.MUTED, key: 'muted', icon: 'times-circle' };
|
||||||
|
default:
|
||||||
|
return { id: NotificationLevels.REGULAR, key: 'regular', icon: 'circle-o' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const all = [ NotificationLevels.WATCHING,
|
||||||
|
NotificationLevels.TRACKING,
|
||||||
|
NotificationLevels.MUTED,
|
||||||
|
NotificationLevels.DEFAULT ].map(buttonDetails);
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
// TODO: This is quite ugly but seems reasonably fast? Maybe refactor
|
||||||
|
// this out before we merge into stable.
|
||||||
|
export function scrollTopFor(y) {
|
||||||
|
let off = 0;
|
||||||
|
for (let i=0; i<3; i++) {
|
||||||
|
off = offsetCalculator(y - off);
|
||||||
|
}
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function offsetCalculator(y) {
|
||||||
|
const $header = $('header');
|
||||||
|
const $title = $('#topic-title');
|
||||||
|
const rawWinHeight = $(window).height();
|
||||||
|
const windowHeight = rawWinHeight - $title.height();
|
||||||
|
const eyeTarget = (windowHeight / 10);
|
||||||
|
const headerHeight = $header.outerHeight(true);
|
||||||
|
const expectedOffset = $title.height() - $header.find('.contents').height() + (eyeTarget * 2);
|
||||||
|
const ideal = headerHeight + ((expectedOffset < 0) ? 0 : expectedOffset);
|
||||||
|
|
||||||
|
const $container = $('.posts-wrapper');
|
||||||
|
const topPos = $container.offset().top;
|
||||||
|
|
||||||
|
const scrollTop = y || $(window).scrollTop();
|
||||||
|
const docHeight = $(document).height();
|
||||||
|
const scrollPercent = (scrollTop / (docHeight-rawWinHeight));
|
||||||
|
|
||||||
|
let inter = topPos - scrollTop + ($container.height() * scrollPercent);
|
||||||
|
if (inter < headerHeight + eyeTarget) {
|
||||||
|
inter = headerHeight + eyeTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (inter > ideal) {
|
||||||
|
const bottom = $('#topic-bottom').offset().top;
|
||||||
|
const switchPos = bottom - rawWinHeight;
|
||||||
|
if (scrollTop > switchPos) {
|
||||||
|
const p = Math.max(Math.min((scrollTop + inter - switchPos) / rawWinHeight, 1.0), 0.0);
|
||||||
|
return ((1 - p) * ideal) + (p * inter);
|
||||||
|
} else {
|
||||||
|
return ideal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inter;
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { onPageChange } from 'discourse/lib/page-tracker';
|
||||||
import { preventCloak } from 'discourse/widgets/post-stream';
|
import { preventCloak } from 'discourse/widgets/post-stream';
|
||||||
import { h } from 'virtual-dom';
|
import { h } from 'virtual-dom';
|
||||||
import { addFlagProperty } from 'discourse/components/site-header';
|
import { addFlagProperty } from 'discourse/components/site-header';
|
||||||
|
import { addPopupMenuOptionsCallback } from 'discourse/controllers/composer';
|
||||||
|
|
||||||
class PluginApi {
|
class PluginApi {
|
||||||
constructor(version, container) {
|
constructor(version, container) {
|
||||||
|
@ -224,6 +225,26 @@ class PluginApi {
|
||||||
addToolbarCallback(callback);
|
addToolbarCallback(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new button in the options popup menu.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* api.addToolbarPopupMenuOptionsCallback(function(controller) {
|
||||||
|
* return {
|
||||||
|
* action: 'toggleWhisper',
|
||||||
|
* icon: 'eye-slash',
|
||||||
|
* label: 'composer.toggle_whisper',
|
||||||
|
* condition: "canWhisper"
|
||||||
|
* };
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
**/
|
||||||
|
addToolbarPopupMenuOptionsCallback(callback) {
|
||||||
|
addPopupMenuOptionsCallback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook that is called when the post stream is removed from the DOM.
|
* A hook that is called when the post stream is removed from the DOM.
|
||||||
* This advanced hook should be used if you end up wiring up any
|
* This advanced hook should be used if you end up wiring up any
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/*global LockOn:true*/
|
import offsetCalculator from 'discourse/lib/offset-calculator';
|
||||||
|
import LockOn from 'discourse/lib/lock-on';
|
||||||
|
|
||||||
let _jumpScheduled = false;
|
let _jumpScheduled = false;
|
||||||
const rewrites = [];
|
const rewrites = [];
|
||||||
|
|
||||||
|
@ -14,14 +16,6 @@ const DiscourseURL = Ember.Object.extend({
|
||||||
// Jumps to a particular post in the stream
|
// Jumps to a particular post in the stream
|
||||||
jumpToPost(postNumber, opts) {
|
jumpToPost(postNumber, opts) {
|
||||||
const holderId = `#post_${postNumber}`;
|
const holderId = `#post_${postNumber}`;
|
||||||
const offset = () => {
|
|
||||||
const $header = $('header');
|
|
||||||
const $title = $('#topic-title');
|
|
||||||
const windowHeight = $(window).height() - $title.height();
|
|
||||||
const expectedOffset = $title.height() - $header.find('.contents').height() + (windowHeight / 5);
|
|
||||||
|
|
||||||
return $header.outerHeight(true) + ((expectedOffset < 0) ? 0 : expectedOffset);
|
|
||||||
};
|
|
||||||
|
|
||||||
Em.run.schedule('afterRender', () => {
|
Em.run.schedule('afterRender', () => {
|
||||||
if (postNumber === 1) {
|
if (postNumber === 1) {
|
||||||
|
@ -29,15 +23,14 @@ const DiscourseURL = Ember.Object.extend({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lockon = new LockOn(holderId, {offsetCalculator: offset});
|
const lockon = new LockOn(holderId);
|
||||||
const holder = $(holderId);
|
const holder = $(holderId);
|
||||||
|
|
||||||
if (holder.length > 0 && opts && opts.skipIfOnScreen){
|
if (holder.length > 0 && opts && opts.skipIfOnScreen){
|
||||||
// if we are on screen skip
|
const elementTop = lockon.elementTop();
|
||||||
const elementTop = lockon.elementTop(),
|
const scrollTop = $(window).scrollTop();
|
||||||
scrollTop = $(window).scrollTop(),
|
const windowHeight = $(window).height() - offsetCalculator();
|
||||||
windowHeight = $(window).height()-offset(),
|
const height = holder.height();
|
||||||
height = holder.height();
|
|
||||||
|
|
||||||
if (elementTop > scrollTop && (elementTop + height) < (scrollTop + windowHeight)) {
|
if (elementTop > scrollTop && (elementTop + height) < (scrollTop + windowHeight)) {
|
||||||
return;
|
return;
|
||||||
|
@ -107,6 +100,8 @@ const DiscourseURL = Ember.Object.extend({
|
||||||
keep the history intact.
|
keep the history intact.
|
||||||
**/
|
**/
|
||||||
routeTo(path, opts) {
|
routeTo(path, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
if (Em.isEmpty(path)) { return; }
|
if (Em.isEmpty(path)) { return; }
|
||||||
|
|
||||||
if (Discourse.get('requiresRefresh')) {
|
if (Discourse.get('requiresRefresh')) {
|
||||||
|
@ -150,12 +145,12 @@ const DiscourseURL = Ember.Object.extend({
|
||||||
|
|
||||||
rewrites.forEach(rw => path = path.replace(rw.regexp, rw.replacement));
|
rewrites.forEach(rw => path = path.replace(rw.regexp, rw.replacement));
|
||||||
|
|
||||||
if (this.navigatedToPost(oldPath, path)) { return; }
|
if (this.navigatedToPost(oldPath, path, opts)) { return; }
|
||||||
// Schedule a DOM cleanup event
|
// Schedule a DOM cleanup event
|
||||||
Em.run.scheduleOnce('afterRender', Discourse.Route, 'cleanDOM');
|
Em.run.scheduleOnce('afterRender', Discourse.Route, 'cleanDOM');
|
||||||
|
|
||||||
// TODO: Extract into rules we can inject into the URL handler
|
// TODO: Extract into rules we can inject into the URL handler
|
||||||
if (this.navigatedToHome(oldPath, path)) { return; }
|
if (this.navigatedToHome(oldPath, path, opts)) { return; }
|
||||||
|
|
||||||
if (oldPath === path) {
|
if (oldPath === path) {
|
||||||
// If navigating to the same path send an app event. Views can watch it
|
// If navigating to the same path send an app event. Views can watch it
|
||||||
|
@ -166,11 +161,11 @@ const DiscourseURL = Ember.Object.extend({
|
||||||
return this.handleURL(path, opts);
|
return this.handleURL(path, opts);
|
||||||
},
|
},
|
||||||
|
|
||||||
rewrite: function(regexp, replacement) {
|
rewrite(regexp, replacement) {
|
||||||
rewrites.push({ regexp: regexp, replacement: replacement });
|
rewrites.push({ regexp, replacement });
|
||||||
},
|
},
|
||||||
|
|
||||||
redirectTo: function(url) {
|
redirectTo(url) {
|
||||||
window.location = Discourse.getURL(url);
|
window.location = Discourse.getURL(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -193,17 +188,11 @@ const DiscourseURL = Ember.Object.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@private
|
|
||||||
|
|
||||||
If the URL is in the topic form, /t/something/:topic_id/:post_number
|
If the URL is in the topic form, /t/something/:topic_id/:post_number
|
||||||
then we want to apply some special logic. If the post_number changes within the
|
then we want to apply some special logic. If the post_number changes within the
|
||||||
same topic, use replaceState and instruct our controller to load more posts.
|
same topic, use replaceState and instruct our controller to load more posts.
|
||||||
|
|
||||||
@method navigatedToPost
|
|
||||||
@param {String} oldPath the previous path we were on
|
|
||||||
@param {String} path the path we're navigating to
|
|
||||||
**/
|
**/
|
||||||
navigatedToPost(oldPath, path) {
|
navigatedToPost(oldPath, path, routeOpts) {
|
||||||
const newMatches = this.TOPIC_REGEXP.exec(path);
|
const newMatches = this.TOPIC_REGEXP.exec(path);
|
||||||
const newTopicId = newMatches ? newMatches[2] : null;
|
const newTopicId = newMatches ? newMatches[2] : null;
|
||||||
|
|
||||||
|
@ -232,14 +221,9 @@ const DiscourseURL = Ember.Object.extend({
|
||||||
enteredAt: new Date().getTime().toString()
|
enteredAt: new Date().getTime().toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
const closestPost = postStream.closestPostForPostNumber(closest);
|
|
||||||
const progress = postStream.progressIndexOfPost(closestPost);
|
|
||||||
const progressController = container.lookup('controller:topic-progress');
|
|
||||||
|
|
||||||
progressController.set('progressPosition', progress);
|
|
||||||
this.appEvents.trigger('post:highlight', closest);
|
this.appEvents.trigger('post:highlight', closest);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
DiscourseURL.jumpToPost(closest, {skipIfOnScreen: true});
|
DiscourseURL.jumpToPost(closest, {skipIfOnScreen: routeOpts.skipIfOnScreen});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Abort routing, we have replaced our state.
|
// Abort routing, we have replaced our state.
|
||||||
|
|
|
@ -91,7 +91,7 @@ export default function userSearch(options) {
|
||||||
|
|
||||||
return new Ember.RSVP.Promise(function(resolve) {
|
return new Ember.RSVP.Promise(function(resolve) {
|
||||||
// TODO site setting for allowed regex in username
|
// TODO site setting for allowed regex in username
|
||||||
if (term.match(/[^a-zA-Z0-9_\.\-]/)) {
|
if (term.match(/[^\w\.\-]/)) {
|
||||||
resolve([]);
|
resolve([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default Ember.Mixin.create({
|
||||||
|
init() {
|
||||||
|
this._super();
|
||||||
|
(this.get('delegated') || []).forEach(m => this.set(m, m));
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,38 @@
|
||||||
|
const helper = {
|
||||||
|
offset() {
|
||||||
|
const mainOffset = $('#main').offset();
|
||||||
|
const offsetTop = mainOffset ? mainOffset.top : 0;
|
||||||
|
return (window.pageYOffset || $('html').scrollTop()) - offsetTop;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Ember.Mixin.create({
|
||||||
|
queueDockCheck: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super();
|
||||||
|
this.queueDockCheck = () => {
|
||||||
|
Ember.run.debounce(this, this.safeDockCheck, 5);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
safeDockCheck() {
|
||||||
|
if (this.isDestroyed || this.isDestroying) { return; }
|
||||||
|
this.dockCheck(helper);
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
|
||||||
|
$(window).bind('scroll.discourse-dock', this.queueDockCheck);
|
||||||
|
$(document).bind('touchmove.discourse-dock', this.queueDockCheck);
|
||||||
|
|
||||||
|
this.dockCheck(helper);
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
this._super();
|
||||||
|
$(window).unbind('scroll.discourse-dock', this.queueDockCheck);
|
||||||
|
$(document).unbind('touchmove.discourse-dock', this.queueDockCheck);
|
||||||
|
}
|
||||||
|
});
|
|
@ -12,13 +12,14 @@ export default Ember.Mixin.create({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
openComposerWithTopicParams(controller, topicTitle, topicBody, topicCategoryId, topicCategory) {
|
openComposerWithTopicParams(controller, topicTitle, topicBody, topicCategoryId, topicCategory, topicTags) {
|
||||||
this.controllerFor('composer').open({
|
this.controllerFor('composer').open({
|
||||||
action: Composer.CREATE_TOPIC,
|
action: Composer.CREATE_TOPIC,
|
||||||
topicTitle,
|
topicTitle,
|
||||||
topicBody,
|
topicBody,
|
||||||
topicCategoryId,
|
topicCategoryId,
|
||||||
topicCategory,
|
topicCategory,
|
||||||
|
topicTags,
|
||||||
draftKey: controller.get('model.draft_key'),
|
draftKey: controller.get('model.draft_key'),
|
||||||
draftSequence: controller.get('model.draft_sequence')
|
draftSequence: controller.get('model.draft_sequence')
|
||||||
});
|
});
|
||||||
|
|
|
@ -86,7 +86,9 @@ const Category = RestModel.extend({
|
||||||
allow_badges: this.get('allow_badges'),
|
allow_badges: this.get('allow_badges'),
|
||||||
custom_fields: this.get('custom_fields'),
|
custom_fields: this.get('custom_fields'),
|
||||||
topic_template: this.get('topic_template'),
|
topic_template: this.get('topic_template'),
|
||||||
suppress_from_homepage: this.get('suppress_from_homepage')
|
suppress_from_homepage: this.get('suppress_from_homepage'),
|
||||||
|
allowed_tags: this.get('allowed_tags'),
|
||||||
|
allowed_tag_groups: this.get('allowed_tag_groups')
|
||||||
},
|
},
|
||||||
type: this.get('id') ? 'PUT' : 'POST'
|
type: this.get('id') ? 'PUT' : 'POST'
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
/**
|
|
||||||
Represents a pop up message displayed over the composer
|
|
||||||
|
|
||||||
@class ComposerMessage
|
|
||||||
@extends Ember.Object
|
|
||||||
@namespace Discourse
|
|
||||||
@module Discourse
|
|
||||||
**/
|
|
||||||
Discourse.ComposerMessage = Em.Object.extend({});
|
|
||||||
|
|
||||||
Discourse.ComposerMessage.reopenClass({
|
|
||||||
/**
|
|
||||||
Look for composer messages given the current composing settings.
|
|
||||||
|
|
||||||
@method find
|
|
||||||
@param {Discourse.Composer} composer The current composer
|
|
||||||
@returns {Discourse.ComposerMessage} the composer message to display (or null)
|
|
||||||
**/
|
|
||||||
find: function(composer) {
|
|
||||||
|
|
||||||
var data = { composerAction: composer.get('action') },
|
|
||||||
topicId = composer.get('topic.id'),
|
|
||||||
postId = composer.get('post.id');
|
|
||||||
|
|
||||||
if (topicId) { data.topic_id = topicId; }
|
|
||||||
if (postId) { data.post_id = postId; }
|
|
||||||
|
|
||||||
return Discourse.ajax('/composer-messages', { data: data }).then(function (messages) {
|
|
||||||
return messages.map(function (message) {
|
|
||||||
return Discourse.ComposerMessage.create(message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||||
|
|
||||||
const Invite = Discourse.Model.extend({
|
const Invite = Discourse.Model.extend({
|
||||||
|
|
||||||
rescind() {
|
rescind() {
|
||||||
|
@ -9,11 +11,13 @@ const Invite = Discourse.Model.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
reinvite() {
|
reinvite() {
|
||||||
Discourse.ajax('/invites/reinvite', {
|
const self = this;
|
||||||
|
return Discourse.ajax('/invites/reinvite', {
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: { email: this.get('email') }
|
data: { email: this.get('email') }
|
||||||
});
|
}).then(function() {
|
||||||
this.set('reinvited', true);
|
self.set('reinvited', true);
|
||||||
|
}).catch(popupAjaxError);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -48,6 +52,10 @@ Invite.reopenClass({
|
||||||
findInvitedCount(user) {
|
findInvitedCount(user) {
|
||||||
if (!user) { return Em.RSVP.resolve(); }
|
if (!user) { return Em.RSVP.resolve(); }
|
||||||
return Discourse.ajax("/users/" + user.get('username_lower') + "/invited_count.json").then(result => Em.Object.create(result.counts));
|
return Discourse.ajax("/users/" + user.get('username_lower') + "/invited_count.json").then(result => Em.Object.create(result.counts));
|
||||||
|
},
|
||||||
|
|
||||||
|
reinviteAll() {
|
||||||
|
return Discourse.ajax('/invites/reinvite-all', { type: 'POST' });
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,7 @@ export default RestModel.extend({
|
||||||
loadingFilter: null,
|
loadingFilter: null,
|
||||||
stagingPost: null,
|
stagingPost: null,
|
||||||
postsWithPlaceholders: null,
|
postsWithPlaceholders: null,
|
||||||
|
timelineLookup: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._identityMap = {};
|
this._identityMap = {};
|
||||||
|
@ -33,6 +34,7 @@ export default RestModel.extend({
|
||||||
loadingBelow: false,
|
loadingBelow: false,
|
||||||
loadingFilter: false,
|
loadingFilter: false,
|
||||||
stagingPost: false,
|
stagingPost: false,
|
||||||
|
timelineLookup: []
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -217,7 +219,7 @@ export default RestModel.extend({
|
||||||
// Request a topicView
|
// Request a topicView
|
||||||
return loadTopicView(topic, opts).then(json => {
|
return loadTopicView(topic, opts).then(json => {
|
||||||
this.updateFromJson(json.post_stream);
|
this.updateFromJson(json.post_stream);
|
||||||
this.setProperties({ loadingFilter: false, loaded: true });
|
this.setProperties({ loadingFilter: false, timelineLookup: json.timeline_lookup, loaded: true });
|
||||||
}).catch(result => {
|
}).catch(result => {
|
||||||
this.errorLoading(result);
|
this.errorLoading(result);
|
||||||
throw result;
|
throw result;
|
||||||
|
@ -612,6 +614,27 @@ export default RestModel.extend({
|
||||||
return closest;
|
return closest;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
closestDaysAgoFor(postNumber) {
|
||||||
|
const timelineLookup = this.get('timelineLookup') || [];
|
||||||
|
|
||||||
|
let low = 0, high = timelineLookup.length - 1;
|
||||||
|
while (low <= high) {
|
||||||
|
const mid = Math.floor(low + ((high - low) / 2));
|
||||||
|
const midValue = timelineLookup[mid][0];
|
||||||
|
|
||||||
|
if (midValue > postNumber) {
|
||||||
|
high = mid - 1;
|
||||||
|
} else if (midValue < postNumber) {
|
||||||
|
low = mid + 1;
|
||||||
|
} else {
|
||||||
|
return timelineLookup[mid][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val = timelineLookup[high] || timelineLookup[low];
|
||||||
|
if (val) { return val[1]; }
|
||||||
|
},
|
||||||
|
|
||||||
// Find a postId for a postNumber, respecting gaps
|
// Find a postId for a postNumber, respecting gaps
|
||||||
findPostIdForPostNumber(postNumber) {
|
findPostIdForPostNumber(postNumber) {
|
||||||
const stream = this.get('stream'),
|
const stream = this.get('stream'),
|
||||||
|
@ -673,6 +696,7 @@ export default RestModel.extend({
|
||||||
const postNumber = post.get('post_number');
|
const postNumber = post.get('post_number');
|
||||||
if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) {
|
if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) {
|
||||||
this.set('topic.highest_post_number', postNumber);
|
this.set('topic.highest_post_number', postNumber);
|
||||||
|
this.set('topic.last_posted_at', post.get('created_at'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import RestModel from 'discourse/models/rest';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
const TagGroup = RestModel.extend({
|
||||||
|
@computed('name', 'tag_names')
|
||||||
|
disableSave() {
|
||||||
|
return Ember.isEmpty(this.get('name')) || Ember.isEmpty(this.get('tag_names')) || this.get('saving');
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
var url = "/tag_groups",
|
||||||
|
self = this;
|
||||||
|
if (this.get('id')) {
|
||||||
|
url = "/tag_groups/" + this.get('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('savingStatus', I18n.t('saving'));
|
||||||
|
this.set('saving', true);
|
||||||
|
|
||||||
|
return Discourse.ajax(url, {
|
||||||
|
data: {
|
||||||
|
name: this.get('name'),
|
||||||
|
tag_names: this.get('tag_names'),
|
||||||
|
parent_tag_name: this.get('parent_tag_name') ? this.get('parent_tag_name') : undefined,
|
||||||
|
one_per_topic: this.get('one_per_topic')
|
||||||
|
},
|
||||||
|
type: this.get('id') ? 'PUT' : 'POST'
|
||||||
|
}).then(function(result) {
|
||||||
|
if(result.id) { self.set('id', result.id); }
|
||||||
|
self.set('savingStatus', I18n.t('saved'));
|
||||||
|
self.set('saving', false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
return Discourse.ajax("/tag_groups/" + this.get('id'), {type: "DELETE"});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TagGroup;
|
|
@ -2,6 +2,7 @@
|
||||||
A model representing a Topic's details that aren't always present, such as a list of participants.
|
A model representing a Topic's details that aren't always present, such as a list of participants.
|
||||||
When showing topics in lists and such this information should not be required.
|
When showing topics in lists and such this information should not be required.
|
||||||
**/
|
**/
|
||||||
|
import NotificationLevels from 'discourse/lib/notification-levels';
|
||||||
import RestModel from 'discourse/models/rest';
|
import RestModel from 'discourse/models/rest';
|
||||||
|
|
||||||
const TopicDetails = RestModel.extend({
|
const TopicDetails = RestModel.extend({
|
||||||
|
@ -35,20 +36,21 @@ const TopicDetails = RestModel.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
notificationReasonText: function() {
|
notificationReasonText: function() {
|
||||||
var level = this.get('notification_level');
|
let level = this.get('notification_level');
|
||||||
if(typeof level !== 'number'){
|
if (typeof level !== 'number') { level = 1; }
|
||||||
level = 1;
|
|
||||||
|
let localeString = `topic.notifications.reasons.${level}`;
|
||||||
|
if (typeof this.get('notifications_reason_id') === 'number') {
|
||||||
|
const tmp = localeString + "_" + this.get('notifications_reason_id');
|
||||||
|
// some sane protection for missing translations of edge cases
|
||||||
|
if (I18n.lookup(tmp)) { localeString = tmp; }
|
||||||
}
|
}
|
||||||
|
|
||||||
var localeString = "topic.notifications.reasons." + level;
|
if (Discourse.User.currentProp('mailing_list_mode') && level > NotificationLevels.MUTED) {
|
||||||
if (typeof this.get('notifications_reason_id') === 'number') {
|
return I18n.t("topic.notifications.reasons.mailing_list_mode");
|
||||||
var tmp = localeString + "_" + this.get('notifications_reason_id');
|
} else {
|
||||||
// some sane protection for missing translations of edge cases
|
return I18n.t(localeString, { username: Discourse.User.currentProp('username_lower') });
|
||||||
if(I18n.lookup(tmp)){
|
|
||||||
localeString = tmp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return I18n.t(localeString, { username: Discourse.User.currentProp('username_lower') });
|
|
||||||
}.property('notification_level', 'notifications_reason_id'),
|
}.property('notification_level', 'notifications_reason_id'),
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -309,10 +309,10 @@ const Topic = RestModel.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
createInvite(emailOrUsername, groupNames) {
|
createInvite(user, group_names, custom_message) {
|
||||||
return Discourse.ajax("/t/" + this.get('id') + "/invite", {
|
return Discourse.ajax("/t/" + this.get('id') + "/invite", {
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: { user: emailOrUsername, group_names: groupNames }
|
data: { user, group_names, custom_message }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,8 @@ const User = RestModel.extend({
|
||||||
|
|
||||||
mutedTopicsPath: url('/latest?state=muted'),
|
mutedTopicsPath: url('/latest?state=muted'),
|
||||||
|
|
||||||
|
watchingTopicsPath: url('/latest?state=watching'),
|
||||||
|
|
||||||
@computed("username")
|
@computed("username")
|
||||||
username_lower(username) {
|
username_lower(username) {
|
||||||
return username.toLowerCase();
|
return username.toLowerCase();
|
||||||
|
@ -321,10 +323,10 @@ const User = RestModel.extend({
|
||||||
Discourse.SiteSettings['newuser_max_' + type + 's'] > 0;
|
Discourse.SiteSettings['newuser_max_' + type + 's'] > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
createInvite(email, group_names) {
|
createInvite(email, group_names, custom_message) {
|
||||||
return Discourse.ajax('/invites', {
|
return Discourse.ajax('/invites', {
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: { email, group_names }
|
data: { email, group_names, custom_message }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -49,9 +49,10 @@ export default function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.resource('group', { path: '/groups/:name' }, function() {
|
this.resource('group', { path: '/groups/:name' }, function() {
|
||||||
|
this.route('members');
|
||||||
|
this.route('posts');
|
||||||
this.route('topics');
|
this.route('topics');
|
||||||
this.route('mentions');
|
this.route('mentions');
|
||||||
this.route('members');
|
|
||||||
this.route('messages');
|
this.route('messages');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -130,4 +131,8 @@ export default function() {
|
||||||
this.route('showParentCategory' + filter.capitalize(), {path: '/c/:parent_category/:category/:tag_id/l/' + filter});
|
this.route('showParentCategory' + filter.capitalize(), {path: '/c/:parent_category/:category/:tag_id/l/' + filter});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.resource('tagGroups', {path: '/tag_groups'}, function() {
|
||||||
|
this.route('show', {path: '/:id'});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,8 +166,8 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
||||||
this.render(w, {into: 'modal/topic-bulk-actions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'});
|
this.render(w, {into: 'modal/topic-bulk-actions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'});
|
||||||
},
|
},
|
||||||
|
|
||||||
createNewTopicViaParams(title, body, category_id, category) {
|
createNewTopicViaParams(title, body, category_id, category, tags) {
|
||||||
this.openComposerWithTopicParams(this.controllerFor('discovery/topics'), title, body, category_id, category);
|
this.openComposerWithTopicParams(this.controllerFor('discovery/topics'), title, body, category_id, category, tags);
|
||||||
},
|
},
|
||||||
|
|
||||||
createNewMessageViaParams(username, title, body) {
|
createNewMessageViaParams(username, title, body) {
|
||||||
|
|
|
@ -1,24 +1,11 @@
|
||||||
export function buildIndex(type) {
|
export default Discourse.Route.extend({
|
||||||
return Discourse.Route.extend({
|
model() {
|
||||||
type,
|
return this.modelFor("group");
|
||||||
|
},
|
||||||
|
|
||||||
model() {
|
setupController(controller, model) {
|
||||||
return this.modelFor("group").findPosts({ type });
|
this.controllerFor("group").set("showing", "members");
|
||||||
},
|
controller.set("model", model);
|
||||||
|
model.findMembers();
|
||||||
setupController(controller, model) {
|
}
|
||||||
this.controllerFor('group-index').setProperties({ model, type });
|
});
|
||||||
this.controllerFor("group").set("showing", type);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderTemplate() {
|
|
||||||
this.render('group-index');
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
didTransition() { return true; }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default buildIndex('posts');
|
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
export default Discourse.Route.extend({
|
export default Discourse.Route.extend({
|
||||||
model() {
|
beforeModel: function() {
|
||||||
return this.modelFor("group");
|
this.transitionTo("group.index");
|
||||||
},
|
|
||||||
|
|
||||||
setupController(controller, model) {
|
|
||||||
this.controllerFor("group").set("showing", "members");
|
|
||||||
controller.set("model", model);
|
|
||||||
model.findMembers();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { buildIndex } from 'discourse/routes/group-index';
|
import { buildGroupPage } from 'discourse/routes/group-posts';
|
||||||
|
|
||||||
export default buildIndex('mentions');
|
export default buildGroupPage('mentions');
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { buildIndex } from 'discourse/routes/group-index';
|
import { buildGroupPage } from 'discourse/routes/group-posts';
|
||||||
|
|
||||||
export default buildIndex('messages');
|
export default buildGroupPage('messages');
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
export function buildGroupPage(type) {
|
||||||
|
return Discourse.Route.extend({
|
||||||
|
type,
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return this.modelFor("group").findPosts({ type });
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
this.controllerFor('group-posts').setProperties({ model, type });
|
||||||
|
this.controllerFor("group").set("showing", type);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderTemplate() {
|
||||||
|
this.render('group-posts');
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
didTransition() { return true; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default buildGroupPage('posts');
|
|
@ -1,3 +1,3 @@
|
||||||
import { buildIndex } from 'discourse/routes/group-index';
|
import { buildGroupPage } from 'discourse/routes/group-posts';
|
||||||
|
|
||||||
export default buildIndex('topics');
|
export default buildGroupPage('topics');
|
||||||
|
|
|
@ -7,7 +7,7 @@ export default Discourse.Route.extend({
|
||||||
if (self.controllerFor('navigation/default').get('canCreateTopic')) {
|
if (self.controllerFor('navigation/default').get('canCreateTopic')) {
|
||||||
// User can create topic
|
// User can create topic
|
||||||
Ember.run.next(function() {
|
Ember.run.next(function() {
|
||||||
e.send('createNewTopicViaParams', transition.queryParams.title, transition.queryParams.body, transition.queryParams.category_id, transition.queryParams.category);
|
e.send('createNewTopicViaParams', transition.queryParams.title, transition.queryParams.body, transition.queryParams.category_id, transition.queryParams.category, transition.queryParams.tags);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model(params) {
|
||||||
|
return this.store.find('tagGroup', params.id);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model() {
|
||||||
|
return this.store.findAll('tagGroup');
|
||||||
|
},
|
||||||
|
|
||||||
|
titleToken() {
|
||||||
|
return I18n.t("tagging.groups.title");
|
||||||
|
},
|
||||||
|
});
|
|
@ -18,6 +18,11 @@ export default Discourse.Route.extend({
|
||||||
didTransition() {
|
didTransition() {
|
||||||
this.controllerFor("application").set("showFooter", true);
|
this.controllerFor("application").set("showFooter", true);
|
||||||
return true;
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
showTagGroups() {
|
||||||
|
this.transitionTo('tagGroups');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,6 @@ export default Discourse.Route.extend({
|
||||||
topic = this.modelFor('topic'),
|
topic = this.modelFor('topic'),
|
||||||
postStream = topic.get('postStream'),
|
postStream = topic.get('postStream'),
|
||||||
topicController = this.controllerFor('topic'),
|
topicController = this.controllerFor('topic'),
|
||||||
topicProgressController = this.controllerFor('topic-progress'),
|
|
||||||
composerController = this.controllerFor('composer');
|
composerController = this.controllerFor('composer');
|
||||||
|
|
||||||
// I sincerely hope no topic gets this many posts
|
// I sincerely hope no topic gets this many posts
|
||||||
|
@ -28,20 +27,15 @@ export default Discourse.Route.extend({
|
||||||
// we need better handling and logging for this condition.
|
// we need better handling and logging for this condition.
|
||||||
|
|
||||||
// The post we requested might not exist. Let's find the closest post
|
// The post we requested might not exist. Let's find the closest post
|
||||||
const closestPost = postStream.closestPostForPostNumber(params.nearPost || 1),
|
const closestPost = postStream.closestPostForPostNumber(params.nearPost || 1);
|
||||||
closest = closestPost.get('post_number'),
|
const closest = closestPost.get('post_number');
|
||||||
progress = postStream.progressIndexOfPost(closestPost);
|
|
||||||
|
|
||||||
topicController.setProperties({
|
topicController.setProperties({
|
||||||
'model.currentPost': closest,
|
'model.currentPost': closest,
|
||||||
|
enteredIndex: postStream.get('stream').indexOf(closestPost.get('id')),
|
||||||
enteredAt: new Date().getTime().toString(),
|
enteredAt: new Date().getTime().toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
topicProgressController.setProperties({
|
|
||||||
progressPosition: progress,
|
|
||||||
expanded: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Highlight our post after the next render
|
// Highlight our post after the next render
|
||||||
Ember.run.scheduleOnce('afterRender', function() {
|
Ember.run.scheduleOnce('afterRender', function() {
|
||||||
self.appEvents.trigger('post:highlight', closest);
|
self.appEvents.trigger('post:highlight', closest);
|
||||||
|
|
|
@ -210,8 +210,6 @@ const TopicRoute = Discourse.Route.extend({
|
||||||
this.topicTrackingState.trackIncoming('all');
|
this.topicTrackingState.trackIncoming('all');
|
||||||
controller.subscribe();
|
controller.subscribe();
|
||||||
|
|
||||||
this.controllerFor('topic-progress').set('model', model);
|
|
||||||
|
|
||||||
// We reset screen tracking every time a topic is entered
|
// We reset screen tracking every time a topic is entered
|
||||||
this.screenTrack.start(model.get('id'), controller);
|
this.screenTrack.start(model.get('id'), controller);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { on, observes } from 'ember-addons/ember-computed-decorators';
|
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
|
||||||
|
|
||||||
const LOGS_NOTICE_KEY = "logs-notice-text";
|
const LOGS_NOTICE_KEY = "logs-notice-text";
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ const LogsNotice = Ember.Object.extend({
|
||||||
|
|
||||||
this.set('text',
|
this.set('text',
|
||||||
I18n.t(`logs_error_rate_notice.${translationKey}`, {
|
I18n.t(`logs_error_rate_notice.${translationKey}`, {
|
||||||
|
relativeAge: autoUpdatingRelativeAge(new Date),
|
||||||
timestamp: moment().format("YYYY-MM-DD H:mm:ss"),
|
timestamp: moment().format("YYYY-MM-DD H:mm:ss"),
|
||||||
siteSettingRate: I18n.t('logs_error_rate_notice.rate', { count: siteSettingLimit, duration: duration }),
|
siteSettingRate: I18n.t('logs_error_rate_notice.rate', { count: siteSettingLimit, duration: duration }),
|
||||||
rate: I18n.t('logs_error_rate_notice.rate', { count: rate, duration: duration }),
|
rate: I18n.t('logs_error_rate_notice.rate', { count: rate, duration: duration }),
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{{#each messages as |message|}}
|
||||||
|
{{composer-message message=message closeMessage="closeMessage"}}
|
||||||
|
{{/each}}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<section class="field">
|
||||||
|
<p>{{i18n 'category.tags_allowed_tags'}}</p>
|
||||||
|
{{tag-chooser placeholderKey="category.tags_placeholder" tags=category.allowed_tags}}
|
||||||
|
|
||||||
|
<p>{{i18n 'category.tags_allowed_tag_groups'}}</p>
|
||||||
|
{{tag-group-chooser placeholderKey="category.tag_groups_placeholder" tagGroups=category.allowed_tag_groups}}
|
||||||
|
</section>
|
|
@ -0,0 +1,13 @@
|
||||||
|
{{#if title}}
|
||||||
|
<h3>{{title}}</h3>
|
||||||
|
{{/if}}
|
||||||
|
{{#if category}}
|
||||||
|
{{category-title-link category=category}}
|
||||||
|
{{/if}}
|
||||||
|
{{#each sortedTags as |tag|}}
|
||||||
|
<div class='tag-box'>
|
||||||
|
{{discourse-tag tag.id}} <span class='tag-count'>x {{tag.count}}</span>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
<div class="clearfix" />
|
||||||
|
<hr/>
|
|
@ -0,0 +1 @@
|
||||||
|
{{yield info}}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{{#unless hidden}}
|
||||||
|
{{#if expanded}}
|
||||||
|
<nav id='topic-progress-expanded'>
|
||||||
|
{{d-button action="jumpTop"
|
||||||
|
disabled=jumpTopDisabled
|
||||||
|
class="full no-text"
|
||||||
|
icon="caret-up"
|
||||||
|
label="topic.progress.go_top"}}
|
||||||
|
<div class='jump-form'>
|
||||||
|
{{input value=toPostIndex}}
|
||||||
|
{{d-button action="jumpPost" label="topic.progress.go"}}
|
||||||
|
</div>
|
||||||
|
{{d-button action="jumpBottom"
|
||||||
|
disabled=jumpBottomDisabled
|
||||||
|
class="full no-text jump-bottom"
|
||||||
|
icon="caret-down"
|
||||||
|
label="topic.progress.go_bottom"}}
|
||||||
|
</nav>
|
||||||
|
{{/if}}
|
||||||
|
<nav id='topic-progress' title="{{i18n 'topic.progress.title'}}" class="{{if hideProgress 'hidden'}}">
|
||||||
|
<div class='nums'>
|
||||||
|
<h4>{{progressPosition}}</h4><span class="{{if hugeNumberOfPosts 'hidden'}}">
|
||||||
|
<span>/</span>
|
||||||
|
<h4>{{postStream.filteredPostsCount}}</h4></span>
|
||||||
|
</div>
|
||||||
|
<i class="fa {{unless expanded 'fa-sort'}}"></i>
|
||||||
|
</nav>
|
||||||
|
{{/unless}}
|
|
@ -3,15 +3,19 @@
|
||||||
|
|
||||||
{{#if currentUser.staff}}
|
{{#if currentUser.staff}}
|
||||||
{{#popup-menu visible=optionsVisible hide="hideOptions" title="composer.options"}}
|
{{#popup-menu visible=optionsVisible hide="hideOptions" title="composer.options"}}
|
||||||
<li>
|
{{#each popupMenuOptions as |option|}}
|
||||||
{{d-button action="toggleWhisper" icon="eye-slash" label="composer.toggle_whisper"}}
|
{{#if option.condition}}
|
||||||
</li>
|
<li>
|
||||||
|
{{d-button action=option.action icon=option.icon label=option.label}}
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
{{/popup-menu}}
|
{{/popup-menu}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{render "composer-messages"}}
|
{{composer-messages composer=model messageCount=messageCount addLinkLookup="addLinkLookup"}}
|
||||||
<div class='control'>
|
|
||||||
|
|
||||||
|
<div class='control'>
|
||||||
{{#if site.mobileView}}
|
{{#if site.mobileView}}
|
||||||
<a href class='toggle-toolbar' {{action "toggleToolbar" bubbles=false}}></a>
|
<a href class='toggle-toolbar' {{action "toggleToolbar" bubbles=false}}></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -86,19 +90,21 @@
|
||||||
composer=model
|
composer=model
|
||||||
lastValidatedAt=lastValidatedAt
|
lastValidatedAt=lastValidatedAt
|
||||||
canWhisper=canWhisper
|
canWhisper=canWhisper
|
||||||
|
popupMenuOptions=popupMenuOptions
|
||||||
draftStatus=model.draftStatus
|
draftStatus=model.draftStatus
|
||||||
isUploading=isUploading
|
isUploading=isUploading
|
||||||
groupsMentioned="groupsMentioned"
|
groupsMentioned="groupsMentioned"
|
||||||
importQuote="importQuote"
|
importQuote="importQuote"
|
||||||
showOptions="showOptions"
|
showOptions="showOptions"
|
||||||
showToolbar=showToolbar
|
showToolbar=showToolbar
|
||||||
showUploadSelector="showUploadSelector"}}
|
showUploadSelector="showUploadSelector"
|
||||||
|
afterRefresh="afterRefresh"}}
|
||||||
|
|
||||||
{{#if currentUser}}
|
{{#if currentUser}}
|
||||||
<div class='submit-panel'>
|
<div class='submit-panel'>
|
||||||
{{plugin-outlet "composer-fields-below"}}
|
{{plugin-outlet "composer-fields-below"}}
|
||||||
{{#if canEditTags}}
|
{{#if canEditTags}}
|
||||||
{{tag-chooser tags=model.tags tabIndex="4"}}
|
{{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<button {{action "save"}} tabindex="5" class="btn btn-primary create {{if disableSubmit 'disabled'}}" title="{{i18n 'composer.title'}}">{{{model.saveIcon}}}{{model.saveText}}</button>
|
<button {{action "save"}} tabindex="5" class="btn btn-primary create {{if disableSubmit 'disabled'}}" title="{{i18n 'composer.title'}}">{{{model.saveIcon}}}{{model.saveText}}</button>
|
||||||
<a href {{action "cancel"}} class='cancel' tabindex="6">{{i18n 'cancel'}}</a>
|
<a href {{action "cancel"}} class='cancel' tabindex="6">{{i18n 'cancel'}}</a>
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
<a href {{action "closeMessage"}} class='close'>{{fa-icon "close"}}</a>
|
||||||
|
<p>{{{message.body}}}</p>
|
|
@ -1,2 +1,2 @@
|
||||||
<a href {{action "closeMessage" this}} class='close'><i class='fa fa-times'></i></a>
|
<a href {{action "closeMessage"}} class='close'>{{fa-icon "times"}}</a>
|
||||||
{{{body}}}
|
{{{message.body}}}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
<a href {{action "closeMessage" this}} class='close'><i class='fa fa-close'></i></a>
|
<a href {{action "closeMessage"}} class='close'>{{fa-icon "close"}}</a>
|
||||||
{{{body}}}
|
{{{message.body}}}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<a href {{action "closeMessage" this}} class='close'>{{fa-icon "close"}}</a>
|
<a href {{action "closeMessage"}} class='close'>{{fa-icon "close"}}</a>
|
||||||
<h3>{{i18n 'composer.similar_topics'}}</h3>
|
<h3>{{i18n 'composer.similar_topics'}}</h3>
|
||||||
|
|
||||||
<ul class='topics'>
|
<ul class='topics'>
|
||||||
{{mount-widget widget="search-result-topic" args=(as-hash results=similarTopics)}}
|
{{mount-widget widget="search-result-topic" args=(as-hash results=message.similarTopics)}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1 +1,45 @@
|
||||||
{{group-post-stream posts=model emptyText=emptyText loadMore="loadMore"}}
|
{{#if model}}
|
||||||
|
{{#if isOwner}}
|
||||||
|
<div class='clearfix'>
|
||||||
|
<form id='add-user-to-group' autocomplete="off">
|
||||||
|
{{user-selector usernames=usernames placeholderKey="groups.selector_placeholder" id="user-search-selector" name="usernames"}}
|
||||||
|
{{d-button action="addMembers" class="add" icon="plus" label="groups.add"}}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#load-more selector=".group-members tr" action="loadMore"}}
|
||||||
|
<table class='group-members'>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">{{i18n 'last_post'}}</th>
|
||||||
|
<th>{{i18n 'last_seen'}}</th>
|
||||||
|
{{#if isOwner}}
|
||||||
|
<th></th>
|
||||||
|
{{/if}}
|
||||||
|
</tr>
|
||||||
|
{{#each model.members as |m|}}
|
||||||
|
<tr>
|
||||||
|
<td class='avatar'>
|
||||||
|
{{user-info user=m}}
|
||||||
|
{{#if m.owner}}<span class='is-owner'>{{i18n "groups.owner"}}</span>{{/if}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text">{{bound-date m.last_posted_at}}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text">{{bound-date m.last_seen_at}}</span>
|
||||||
|
</td>
|
||||||
|
{{#if isOwner}}
|
||||||
|
<td class='remove-user'>
|
||||||
|
{{#unless m.owner}}
|
||||||
|
<a class="remove-link" {{action "removeMember" m}}><i class="fa fa-times"></i></a>
|
||||||
|
{{/unless}}
|
||||||
|
</td>
|
||||||
|
{{/if}}
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
{{/load-more}}
|
||||||
|
{{else}}
|
||||||
|
<div>{{i18n "groups.empty.users"}}</div>
|
||||||
|
{{/if}}
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
{{#if model}}
|
|
||||||
{{#if isOwner}}
|
|
||||||
<div class='clearfix'>
|
|
||||||
<form id='add-user-to-group' autocomplete="off">
|
|
||||||
{{user-selector usernames=usernames placeholderKey="groups.selector_placeholder" id="user-search-selector" name="usernames"}}
|
|
||||||
{{d-button action="addMembers" class="add" icon="plus" label="groups.add"}}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#load-more selector=".group-members tr" action="loadMore"}}
|
|
||||||
<table class='group-members'>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">{{i18n 'last_post'}}</th>
|
|
||||||
<th>{{i18n 'last_seen'}}</th>
|
|
||||||
{{#if isOwner}}
|
|
||||||
<th></th>
|
|
||||||
{{/if}}
|
|
||||||
</tr>
|
|
||||||
{{#each model.members as |m|}}
|
|
||||||
<tr>
|
|
||||||
<td class='avatar'>
|
|
||||||
{{user-info user=m}}
|
|
||||||
{{#if m.owner}}<span class='is-owner'>{{i18n "groups.owner"}}</span>{{/if}}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="text">{{bound-date m.last_posted_at}}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="text">{{bound-date m.last_seen_at}}</span>
|
|
||||||
</td>
|
|
||||||
{{#if isOwner}}
|
|
||||||
<td class='remove-user'>
|
|
||||||
{{#unless m.owner}}
|
|
||||||
<a class="remove-link" {{action "removeMember" m}}><i class="fa fa-times"></i></a>
|
|
||||||
{{/unless}}
|
|
||||||
</td>
|
|
||||||
{{/if}}
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</table>
|
|
||||||
{{/load-more}}
|
|
||||||
{{else}}
|
|
||||||
<div>{{i18n "groups.empty.users"}}</div>
|
|
||||||
{{/if}}
|
|
|
@ -0,0 +1 @@
|
||||||
|
{{group-post-stream posts=model emptyText=emptyText loadMore="loadMore"}}
|
|
@ -25,7 +25,7 @@
|
||||||
<tr class="input">
|
<tr class="input">
|
||||||
<td class="label"><label for='new-account-username'>{{i18n 'user.username.title'}}</label></td>
|
<td class="label"><label for='new-account-username'>{{i18n 'user.username.title'}}</label></td>
|
||||||
<td>
|
<td>
|
||||||
{{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength}}
|
{{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="off"}}
|
||||||
{{input-tip validation=usernameValidation id="username-validation"}}
|
{{input-tip validation=usernameValidation id="username-validation"}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
{{edit-category-tab panels=panels selectedTab=selectedTab tab="settings"}}
|
{{edit-category-tab panels=panels selectedTab=selectedTab tab="settings"}}
|
||||||
{{edit-category-tab panels=panels selectedTab=selectedTab tab="images"}}
|
{{edit-category-tab panels=panels selectedTab=selectedTab tab="images"}}
|
||||||
{{edit-category-tab panels=panels selectedTab=selectedTab tab="topic-template"}}
|
{{edit-category-tab panels=panels selectedTab=selectedTab tab="topic-template"}}
|
||||||
|
{{#if siteSettings.tagging_enabled}}
|
||||||
|
{{edit-category-tab panels=panels selectedTab=selectedTab tab="tags"}}
|
||||||
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
|
@ -19,9 +19,15 @@
|
||||||
{{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}}
|
{{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if showGroups}}
|
{{#if showGroups}}
|
||||||
<label>{{{groupInstructions}}}</label>
|
<label><span class={{showGroupsClass}}>{{i18n 'topic.automatically_add_to_groups'}}</span></label>
|
||||||
{{group-selector groupFinder=groupFinder groupNames=model.groupNames placeholderKey="topic.invite_private.group_name"}}
|
{{group-selector groupFinder=groupFinder groupNames=model.groupNames placeholderKey="topic.invite_private.group_name"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if showCustomMessage}}
|
||||||
|
<br><label><span class='optional'>{{i18n 'invite.custom_message'}}</span> <a {{action "showCustomMessageBox"}}>{{i18n 'invite.custom_message_link'}}</a>.</label>
|
||||||
|
{{#if hasCustomMessage}}{{textarea value=customMessage placeholder=customMessagePlaceholder}}{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|
|
@ -14,6 +14,12 @@
|
||||||
{{share-source source=s title=title action="share"}}
|
{{share-source source=s title=title action="share"}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
|
{{#if canReplyAsNewTopic}}
|
||||||
|
<div class='reply-as-new-topic'>
|
||||||
|
<a href {{action "replyAsNewTopic"}} aria-label='{{i18n 'post.reply_as_new_topic'}}' title='{{i18n 'post.reply_as_new_topic'}}'>{{fa-icon "plus"}}{{i18n 'topic.create'}}</a>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<div class='link'>
|
<div class='link'>
|
||||||
<a href {{action "close"}} aria-label='{{i18n 'share.close'}}' title='{{i18n 'share.close'}}'>{{fa-icon "close"}}</a>
|
<a href {{action "close"}} aria-label='{{i18n 'share.close'}}' title='{{i18n 'share.close'}}'>{{fa-icon "close"}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="tag-group-content">
|
||||||
|
<p class="about">{{i18n 'tagging.groups.about'}}</p>
|
||||||
|
</div>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<div class="tag-group-content">
|
||||||
|
<h1>{{text-field value=model.name}}</h1>
|
||||||
|
<br/>
|
||||||
|
<section class="group-tags-list">
|
||||||
|
<label>{{i18n 'tagging.groups.tags_label'}}</label><br/>
|
||||||
|
{{tag-chooser tags=model.tag_names everyTag="true" unlimitedTagCount="true"}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="parent-tag-section">
|
||||||
|
<label>{{i18n 'tagging.groups.parent_tag_label'}}</label>
|
||||||
|
{{tag-chooser tags=model.parent_tag_name everyTag="true" limit="1" placeholderKey="tagging.groups.parent_tag_placeholder"}}
|
||||||
|
<span class="description">{{i18n 'tagging.groups.parent_tag_description'}}</span>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="group-one-per-topic">
|
||||||
|
<label>
|
||||||
|
{{input type="checkbox" checked=model.one_per_topic name="onepertopic"}}
|
||||||
|
{{i18n 'tagging.groups.one_per_topic_label'}}
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button {{action "save"}} disabled={{model.disableSave}} class='btn'>{{i18n 'tagging.groups.save'}}</button>
|
||||||
|
<button {{action "destroy"}} disabled={{model.disableSave}} class='btn btn-danger'><i class="fa fa-trash-o"></i> {{i18n 'tagging.groups.delete'}}</button>
|
||||||
|
<span class="saving {{unless model.savingStatus 'hidden'}}">{{model.savingStatus}}</span>
|
||||||
|
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue