mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-24 08:09:13 -05:00
commit
bd509f5550
447 changed files with 13720 additions and 5761 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -106,3 +106,4 @@ config/version.rb
|
|||
bundler_stubs/*
|
||||
|
||||
vendor/bundle/*
|
||||
*.db
|
||||
|
|
|
@ -45,6 +45,7 @@ before_install:
|
|||
- eslint app/assets/javascripts
|
||||
- eslint --ext .es6 app/assets/javascripts
|
||||
- eslint --ext .es6 test/javascripts
|
||||
- eslint --ext .es6 plugins/**/assets/javascripts
|
||||
- eslint test/javascripts
|
||||
|
||||
before_script:
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -61,7 +61,7 @@ gem 'fast_xs'
|
|||
gem 'fast_xor'
|
||||
|
||||
# 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 'excon', require: false
|
||||
gem 'unf', require: false
|
||||
|
@ -74,7 +74,7 @@ gem 'email_reply_trimmer', '0.1.3'
|
|||
gem 'image_optim', '0.20.2'
|
||||
gem 'multi_json'
|
||||
gem 'mustache'
|
||||
gem 'nokogiri', '1.6.8.rc3'
|
||||
gem 'nokogiri'
|
||||
gem 'omniauth'
|
||||
gem 'omniauth-openid'
|
||||
gem 'openid-redis-store'
|
||||
|
@ -125,7 +125,7 @@ group :test do
|
|||
end
|
||||
|
||||
group :test, :development do
|
||||
gem 'rspec', '~> 3.2.0'
|
||||
gem 'rspec'
|
||||
gem 'mock_redis'
|
||||
gem 'listen', '0.7.3', require: false
|
||||
gem 'certified', require: false
|
||||
|
|
44
Gemfile.lock
44
Gemfile.lock
|
@ -73,7 +73,7 @@ GEM
|
|||
diff-lcs (1.2.5)
|
||||
discourse-qunit-rails (0.0.9)
|
||||
railties
|
||||
discourse_fastimage (2.0.0)
|
||||
discourse_fastimage (2.0.2)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.25)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
|
@ -174,7 +174,7 @@ GEM
|
|||
multipart-post (2.0.0)
|
||||
mustache (1.0.3)
|
||||
netrc (0.11.0)
|
||||
nokogiri (1.6.8.rc3)
|
||||
nokogiri (1.6.8)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
pkg-config (~> 1.1.7)
|
||||
nokogumbo (1.4.7)
|
||||
|
@ -294,33 +294,33 @@ GEM
|
|||
netrc (~> 0.7)
|
||||
rinku (2.0.0)
|
||||
rmmseg-cpp (0.2.9)
|
||||
rspec (3.2.0)
|
||||
rspec-core (~> 3.2.0)
|
||||
rspec-expectations (~> 3.2.0)
|
||||
rspec-mocks (~> 3.2.0)
|
||||
rspec-core (3.2.3)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-expectations (3.2.1)
|
||||
rspec (3.4.0)
|
||||
rspec-core (~> 3.4.0)
|
||||
rspec-expectations (~> 3.4.0)
|
||||
rspec-mocks (~> 3.4.0)
|
||||
rspec-core (3.4.4)
|
||||
rspec-support (~> 3.4.0)
|
||||
rspec-expectations (3.4.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-support (~> 3.4.0)
|
||||
rspec-given (3.7.1)
|
||||
given_core (= 3.7.1)
|
||||
rspec (>= 2.14.0)
|
||||
rspec-html-matchers (0.7.0)
|
||||
nokogiri (~> 1)
|
||||
rspec (~> 3)
|
||||
rspec-mocks (3.2.1)
|
||||
rspec-mocks (3.4.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-rails (3.2.3)
|
||||
rspec-support (~> 3.4.0)
|
||||
rspec-rails (3.4.2)
|
||||
actionpack (>= 3.0, < 4.3)
|
||||
activesupport (>= 3.0, < 4.3)
|
||||
railties (>= 3.0, < 4.3)
|
||||
rspec-core (~> 3.2.0)
|
||||
rspec-expectations (~> 3.2.0)
|
||||
rspec-mocks (~> 3.2.0)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-support (3.2.2)
|
||||
rspec-core (~> 3.4.0)
|
||||
rspec-expectations (~> 3.4.0)
|
||||
rspec-mocks (~> 3.4.0)
|
||||
rspec-support (~> 3.4.0)
|
||||
rspec-support (3.4.1)
|
||||
rtlit (0.0.5)
|
||||
ruby-openid (2.7.0)
|
||||
ruby-readability (0.7.0)
|
||||
|
@ -410,7 +410,7 @@ DEPENDENCIES
|
|||
byebug
|
||||
certified
|
||||
discourse-qunit-rails
|
||||
discourse_fastimage
|
||||
discourse_fastimage (= 2.0.2)
|
||||
email_reply_trimmer (= 0.1.3)
|
||||
ember-rails (= 0.18.5)
|
||||
ember-source (= 1.12.2)
|
||||
|
@ -444,7 +444,7 @@ DEPENDENCIES
|
|||
mock_redis
|
||||
multi_json
|
||||
mustache
|
||||
nokogiri (= 1.6.8.rc3)
|
||||
nokogiri
|
||||
oj
|
||||
omniauth
|
||||
omniauth-facebook
|
||||
|
@ -475,7 +475,7 @@ DEPENDENCIES
|
|||
rest-client
|
||||
rinku
|
||||
rmmseg-cpp
|
||||
rspec (~> 3.2.0)
|
||||
rspec
|
||||
rspec-given
|
||||
rspec-html-matchers
|
||||
rspec-rails
|
||||
|
@ -500,4 +500,4 @@ DEPENDENCIES
|
|||
unicorn
|
||||
|
||||
BUNDLED WITH
|
||||
1.12.3
|
||||
1.12.5
|
||||
|
|
6
Vagrantfile
vendored
6
Vagrantfile
vendored
|
@ -3,8 +3,8 @@
|
|||
# See https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md
|
||||
#
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = 'discourse/discourse-1.3.0'
|
||||
config.vm.box_url = "http://discourse-vms.s3.amazonaws.com/discourse-1.3.0.box"
|
||||
config.vm.box = 'discourse-16.04'
|
||||
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
|
||||
# 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
|
||||
|
||||
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
|
||||
|
|
|
@ -65,7 +65,11 @@ export default Ember.Component.extend({
|
|||
|
||||
const $elem = this.$();
|
||||
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');
|
||||
$elem.on("change", e => {
|
||||
|
|
|
@ -82,7 +82,7 @@ export default Ember.Component.extend({
|
|||
}
|
||||
|
||||
this._bindUploadTarget();
|
||||
this.appEvents.trigger('composer:opened');
|
||||
this.appEvents.trigger('composer:will-open');
|
||||
},
|
||||
|
||||
@computed('composer.reply', 'composer.replyLength', 'composer.missingReplyCharacters', 'composer.minimumPostLength', 'lastValidatedAt')
|
||||
|
@ -341,6 +341,7 @@ export default Ember.Component.extend({
|
|||
|
||||
@on('willDestroyElement')
|
||||
_composerClosed() {
|
||||
this.appEvents.trigger('composer:will-close');
|
||||
Ember.run.next(() => {
|
||||
$('#main-outlet').css('padding-bottom', 0);
|
||||
// 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);
|
||||
},
|
||||
|
||||
showOptions() {
|
||||
showOptions(toolbarEvent) {
|
||||
// long term we want some smart positioning algorithm in popup-menu
|
||||
// the problem is that positioning in a fixed panel is a nightmare
|
||||
// cause offsetParent can end up returning a fixed element and then
|
||||
|
@ -387,9 +388,8 @@ export default Ember.Component.extend({
|
|||
left = replyWidth - popupWidth - 40;
|
||||
}
|
||||
|
||||
this.sendAction('showOptions', { position: "absolute",
|
||||
left: left,
|
||||
top: top });
|
||||
this.sendAction('showOptions', toolbarEvent,
|
||||
{ position: "absolute", left, top });
|
||||
},
|
||||
|
||||
showUploadModal(toolbarEvent) {
|
||||
|
@ -419,7 +419,7 @@ export default Ember.Component.extend({
|
|||
sendAction: 'showUploadModal'
|
||||
});
|
||||
|
||||
if (this.get('canWhisper')) {
|
||||
if (this.get("popupMenuOptions").some(option => option.condition)) {
|
||||
toolbar.addButton({
|
||||
id: 'options',
|
||||
group: 'extras',
|
||||
|
@ -458,6 +458,7 @@ export default Ember.Component.extend({
|
|||
// Paint oneboxes
|
||||
$('a.onebox', $preview).each((i, e) => Discourse.Onebox.load(e, refresh));
|
||||
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,
|
||||
_renderCallback: null,
|
||||
_childEvents: null,
|
||||
_dispatched: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
const name = this.get('widget');
|
||||
|
||||
(this.get('delegated') || []).forEach(m => this.set(m, m));
|
||||
|
||||
this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`);
|
||||
|
||||
if (!this._widgetClass) {
|
||||
|
@ -29,6 +32,7 @@ export default Ember.Component.extend({
|
|||
|
||||
this._childEvents = [];
|
||||
this._connected = [];
|
||||
this._dispatched = [];
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
|
@ -50,7 +54,10 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
|
@ -71,9 +78,10 @@ export default Ember.Component.extend({
|
|||
|
||||
dispatch(eventName, key) {
|
||||
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) {
|
||||
|
@ -93,7 +101,6 @@ export default Ember.Component.extend({
|
|||
if (!this._widgetClass) { return; }
|
||||
|
||||
const t0 = new Date().getTime();
|
||||
|
||||
const args = this.get('args') || this.buildArgs();
|
||||
const opts = { model: this.get('model') };
|
||||
const newTree = new this._widgetClass(args, this.container, opts);
|
||||
|
@ -117,8 +124,6 @@ export default Ember.Component.extend({
|
|||
if (this.profileWidget) {
|
||||
console.log(new Date().getTime() - t0);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -1,73 +1,49 @@
|
|||
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'],
|
||||
title: '',
|
||||
buttonIncludesText: true,
|
||||
activeItem: Em.computed.alias('notificationLevel'),
|
||||
i18nPrefix: '',
|
||||
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() {
|
||||
return [['WATCHING', 'watching', this.watchingClasses],
|
||||
['TRACKING', 'tracking', this.trackingClasses],
|
||||
['REGULAR', 'regular', this.regularClasses],
|
||||
['MUTED', 'muted', this.mutedClasses]];
|
||||
}.property(),
|
||||
@computed
|
||||
dropDownContent() {
|
||||
const prefix = this.get('i18nPrefix');
|
||||
const postfix = this.get('i18nPostfix');
|
||||
|
||||
dropDownContent: function() {
|
||||
const contents = [],
|
||||
prefix = this.get('i18nPrefix'),
|
||||
postfix = this.get('i18nPostfix');
|
||||
|
||||
_.each(this.get('options'), function(pair) {
|
||||
if (postfix === '_pm' && pair[1] === 'regular') { return; }
|
||||
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 all.map(l => {
|
||||
const start = `${prefix}.${l.key}${postfix}`;
|
||||
return {
|
||||
id: l.id,
|
||||
title: I18n.t(`${start}.title`),
|
||||
description: I18n.t(`${start}.description`),
|
||||
styleClasses: `${l.key} fa fa-${l.icon}`
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
return contents;
|
||||
}.property(),
|
||||
@computed('notificationLevel')
|
||||
text(notificationLevel) {
|
||||
const details = buttonDetails(notificationLevel);
|
||||
const { key } = details;
|
||||
const icon = iconHTML(details.icon, { class: key });
|
||||
|
||||
text: function() {
|
||||
const self = this,
|
||||
prefix = this.get('i18nPrefix'),
|
||||
postfix = this.get('i18nPostfix');
|
||||
|
||||
const key = (function() {
|
||||
switch (this.get('notificationLevel')) {
|
||||
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'),
|
||||
if (this.get('buttonIncludesText')) {
|
||||
const prefix = this.get('i18nPrefix');
|
||||
const postfix = this.get('i18nPostfix');
|
||||
const text = I18n.t(`${prefix}.${key}${postfix}.title`);
|
||||
return `${icon} ${text}<span class='caret'></span>`;
|
||||
} else {
|
||||
return `${icon} <span class='caret'></span>`;
|
||||
}
|
||||
},
|
||||
|
||||
clicked(/* id */) {
|
||||
// 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 { cloak, uncloak } from 'discourse/widgets/post-stream';
|
||||
import { isWorkaroundActive } from 'discourse/lib/safari-hacks';
|
||||
import offsetCalculator from 'discourse/lib/offset-calculator';
|
||||
|
||||
function findTopView($posts, viewportTop, min, max) {
|
||||
if (max < min) { return min; }
|
||||
|
||||
while(max>min){
|
||||
while (max > min) {
|
||||
const mid = Math.floor((min + max) / 2);
|
||||
const $post = $($posts[mid]);
|
||||
const viewBottom = $post.position().top + $post.height();
|
||||
|
@ -26,8 +27,11 @@ export default MountWidget.extend({
|
|||
widget: 'post-stream',
|
||||
_topVisible: null,
|
||||
_bottomVisible: null,
|
||||
_currentPost: null,
|
||||
_currentVisible: null,
|
||||
_currentPercent: null,
|
||||
|
||||
args: Ember.computed(function() {
|
||||
buildArgs() {
|
||||
return this.getProperties('posts',
|
||||
'canCreatePost',
|
||||
'multiSelect',
|
||||
|
@ -35,7 +39,7 @@ export default MountWidget.extend({
|
|||
'selectedQuery',
|
||||
'selectedPostsCount',
|
||||
'searchService');
|
||||
}).volatile(),
|
||||
},
|
||||
|
||||
beforePatch() {
|
||||
const $body = $(document);
|
||||
|
@ -66,7 +70,7 @@ export default MountWidget.extend({
|
|||
const onscreen = [];
|
||||
const nearby = [];
|
||||
|
||||
let windowTop = $w.scrollTop();
|
||||
const windowTop = $w.scrollTop();
|
||||
|
||||
const $posts = this.$('.onscreen-post, .cloaked-post');
|
||||
const viewportTop = windowTop - slack;
|
||||
|
@ -79,7 +83,18 @@ export default MountWidget.extend({
|
|||
if (windowBottom > bodyHeight) { windowBottom = 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 lastBottom = 0;
|
||||
while (bottomView < $posts.length) {
|
||||
const post = $posts[bottomView];
|
||||
const $post = $(post);
|
||||
|
@ -87,18 +102,34 @@ export default MountWidget.extend({
|
|||
if (!$post) { break; }
|
||||
|
||||
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 (viewBottom > windowTop && viewTop <= windowBottom) {
|
||||
if (viewBottom >= windowTop && viewTop <= windowBottom) {
|
||||
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++;
|
||||
}
|
||||
|
||||
if (allAbove) {
|
||||
if (percent === null) { percent = 1.0; }
|
||||
if (currentPost === null) { currentPost = bottomView - 1; }
|
||||
}
|
||||
|
||||
const posts = this.posts;
|
||||
const refresh = cb => this.queueRerender(cb);
|
||||
if (onscreen.length) {
|
||||
|
@ -131,9 +162,28 @@ export default MountWidget.extend({
|
|||
this._bottomVisible = last;
|
||||
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 {
|
||||
this._topVisible = null;
|
||||
this._bottomVisible = null;
|
||||
this._currentPost = null;
|
||||
this._currentPercent = null;
|
||||
}
|
||||
|
||||
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 { observes } from 'ember-addons/ember-computed-decorators';
|
||||
import Docking from 'discourse/mixins/docking';
|
||||
|
||||
const _flagProperties = [];
|
||||
function addFlagProperty(prop) {
|
||||
|
@ -8,45 +9,37 @@ function addFlagProperty(prop) {
|
|||
|
||||
const PANEL_BODY_MARGIN = 30;
|
||||
|
||||
const SiteHeaderComponent = MountWidget.extend({
|
||||
const SiteHeaderComponent = MountWidget.extend(Docking, {
|
||||
widget: 'header',
|
||||
docAt: null,
|
||||
dockedHeader: null,
|
||||
_topic: null,
|
||||
|
||||
// profileWidget: true,
|
||||
// classNameBindings: ['editingTopic'],
|
||||
|
||||
@observes('currentUser.unread_notifications', 'currentUser.unread_private_messages')
|
||||
_notificationsChanged() {
|
||||
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');
|
||||
|
||||
// Check the dock after the current run loop. While rendering,
|
||||
// it's much slower to calculate `outlet.offset()`
|
||||
Ember.run.next(() => {
|
||||
if (this.docAt === null) {
|
||||
const outlet = $('#main-outlet');
|
||||
if (!(outlet && outlet.length === 1)) return;
|
||||
this.docAt = outlet.offset().top;
|
||||
const offset = info.offset();
|
||||
if (offset >= this.docAt) {
|
||||
if (!this.dockedHeader) {
|
||||
$body.addClass('docked');
|
||||
this.dockedHeader = true;
|
||||
}
|
||||
|
||||
const offset = window.pageYOffset || $('html').scrollTop();
|
||||
if (offset >= this.docAt) {
|
||||
if (!this.dockedHeader) {
|
||||
$body.addClass('docked');
|
||||
this.dockedHeader = true;
|
||||
}
|
||||
} else {
|
||||
if (this.dockedHeader) {
|
||||
$body.removeClass('docked');
|
||||
this.dockedHeader = false;
|
||||
}
|
||||
} else {
|
||||
if (this.dockedHeader) {
|
||||
$body.removeClass('docked');
|
||||
this.dockedHeader = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setTopic(topic) {
|
||||
|
@ -56,8 +49,6 @@ const SiteHeaderComponent = MountWidget.extend({
|
|||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
$(window).bind('scroll.discourse-dock', () => this.examineDockHeader());
|
||||
$(document).bind('touchmove.discourse-dock', () => this.examineDockHeader());
|
||||
$(window).on('resize.discourse-menu-panel', () => this.afterRender());
|
||||
|
||||
this.appEvents.on('header:show-topic', topic => this.setTopic(topic));
|
||||
|
@ -72,16 +63,11 @@ const SiteHeaderComponent = MountWidget.extend({
|
|||
this.eventDispatched('dom:clean', 'header');
|
||||
}
|
||||
});
|
||||
|
||||
this.examineDockHeader();
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
$(window).unbind('scroll.discourse-dock');
|
||||
$(document).unbind('touchmove.discourse-dock');
|
||||
$('body').off('keydown.header');
|
||||
this.appEvents.off('notifications:changed');
|
||||
$(window).off('resize.discourse-menu-panel');
|
||||
|
||||
this.appEvents.off('header:show-topic');
|
||||
|
|
|
@ -6,9 +6,9 @@ function formatTag(t) {
|
|||
|
||||
export default Ember.TextField.extend({
|
||||
classNameBindings: [':tag-chooser'],
|
||||
attributeBindings: ['tabIndex'],
|
||||
attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'],
|
||||
|
||||
_setupTags: function() {
|
||||
_initValue: function() {
|
||||
const tags = this.get('tags') || [];
|
||||
this.set('value', tags.join(", "));
|
||||
}.on('init'),
|
||||
|
@ -18,16 +18,37 @@ export default Ember.TextField.extend({
|
|||
this.set('tags', tags);
|
||||
}.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() {
|
||||
const site = this.site,
|
||||
self = this,
|
||||
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({
|
||||
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,
|
||||
maximumSelectionSize: this.siteSettings.max_tags_per_topic,
|
||||
maximumSelectionSize: limit,
|
||||
initSelection(element, callback) {
|
||||
const data = [];
|
||||
|
||||
|
@ -65,7 +86,7 @@ export default Ember.TextField.extend({
|
|||
list.push(item);
|
||||
},
|
||||
formatSelection: function (data) {
|
||||
return data ? renderTag(this.text(data)) : undefined;
|
||||
return data ? renderTag(this.text(data)) : undefined;
|
||||
},
|
||||
formatSelectionCssClass: function(){
|
||||
return "discourse-tag-select2";
|
||||
|
@ -78,7 +99,16 @@ export default Ember.TextField.extend({
|
|||
url: Discourse.getURL("/tags/filter/search"),
|
||||
dataType: 'json',
|
||||
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) {
|
||||
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')
|
||||
|
||||
});
|
21
app/assets/javascripts/discourse/components/tag-list.js.es6
Normal file
21
app/assets/javascripts/discourse/components/tag-list.js.es6
Normal file
|
@ -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 { on } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default ContainerView.extend({
|
||||
elementId: 'topic-footer-buttons',
|
||||
|
||||
@on('init')
|
||||
createButtons() {
|
||||
const topic = this.get('topic');
|
||||
const currentUser = this.get('controller.currentUser');
|
||||
init() {
|
||||
this._super();
|
||||
|
||||
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, 'pinned-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({
|
||||
longDescription: Em.computed.alias('topic.details.notificationReasonText'),
|
||||
hidden: Em.computed.alias('topic.deleted'),
|
||||
notificationLevel: Em.computed.alias('topic.details.notification_level'),
|
||||
i18nPrefix: 'topic.notifications',
|
||||
export default MountWidget.extend({
|
||||
widget: 'topic-notifications-button',
|
||||
|
||||
i18nPostfix: function() {
|
||||
return this.get('topic.isPrivateMessage') ? '_pm' : '';
|
||||
}.property('topic.isPrivateMessage'),
|
||||
buildArgs() {
|
||||
return { topic: this.get('topic'), appendReason: true, showFullTitle: true };
|
||||
},
|
||||
|
||||
clicked(id) {
|
||||
this.get('topic.details').updateNotifications(id);
|
||||
@observes('topic.details.notification_level')
|
||||
_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 Composer from 'discourse/models/composer';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { relativeAge } from 'discourse/lib/formatter';
|
||||
|
||||
function loadDraft(store, opts) {
|
||||
opts = opts || {};
|
||||
|
@ -41,22 +42,39 @@ function loadDraft(store, opts) {
|
|||
}
|
||||
}
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
needs: ['modal', 'topic', 'composer-messages', 'application'],
|
||||
const _popupMenuOptionsCallbacks = [];
|
||||
|
||||
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),
|
||||
checkedMessages: false,
|
||||
|
||||
messageCount: null,
|
||||
showEditReason: false,
|
||||
editReason: null,
|
||||
scopedCategoryId: null,
|
||||
similarTopics: null,
|
||||
similarTopicsMessage: null,
|
||||
lastSimilaritySearch: null,
|
||||
optionsVisible: false,
|
||||
lastValidatedAt: null,
|
||||
isUploading: false,
|
||||
topic: null,
|
||||
linkLookup: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
|
||||
addPopupMenuOptionsCallback(function() {
|
||||
return {
|
||||
action: 'toggleWhisper',
|
||||
icon: 'eye-slash',
|
||||
label: 'composer.toggle_whisper',
|
||||
condition: "canWhisper"
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
showToolbar: Em.computed({
|
||||
get(){
|
||||
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'),
|
||||
|
||||
_initializeSimilar: function() {
|
||||
this.set('similarTopics', []);
|
||||
}.on('init'),
|
||||
|
||||
@computed('model.canEditTitle', 'model.creatingPrivateMessage')
|
||||
canEditTags(canEditTitle, creatingPrivateMessage) {
|
||||
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;
|
||||
},
|
||||
|
||||
@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() {
|
||||
if (!Discourse.User.currentProp('staff')) { return false; }
|
||||
|
||||
|
@ -111,6 +142,45 @@ export default Ember.Controller.extend({
|
|||
}.property('model.creatingPrivateMessage', 'model.targetUsernames'),
|
||||
|
||||
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() {
|
||||
this.toggleProperty('model.whisper');
|
||||
|
@ -120,7 +190,8 @@ export default Ember.Controller.extend({
|
|||
this.toggleProperty('showToolbar');
|
||||
},
|
||||
|
||||
showOptions(loc) {
|
||||
showOptions(toolbarEvent, loc) {
|
||||
this.set('toolbarEvent', toolbarEvent);
|
||||
this.appEvents.trigger('popup-menu:open', loc);
|
||||
this.set('optionsVisible', true);
|
||||
},
|
||||
|
@ -184,9 +255,8 @@ export default Ember.Controller.extend({
|
|||
},
|
||||
|
||||
hitEsc() {
|
||||
const messages = this.get('controllers.composer-messages.model');
|
||||
if (messages.length) {
|
||||
messages.popObject();
|
||||
if ((this.get('messageCount') || 0) > 0) {
|
||||
this.appEvents.trigger('composer-messages:close');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -203,7 +273,18 @@ export default Ember.Controller.extend({
|
|||
|
||||
groupsMentioned(groups) {
|
||||
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) {
|
||||
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' &&
|
||||
|
@ -360,61 +441,14 @@ export default Ember.Controller.extend({
|
|||
return promise;
|
||||
},
|
||||
|
||||
// Checks to see if a reply has been typed.
|
||||
// This is signaled by a keyUp event in a view.
|
||||
// Notify the composer messages controller that a reply has been typed. Some
|
||||
// messages only appear after typing.
|
||||
checkReplyLength() {
|
||||
if (!Ember.isEmpty('model.reply')) {
|
||||
// Notify the composer messages controller that a reply has been typed. Some
|
||||
// messages only appear after typing.
|
||||
this.get('controllers.composer-messages').typedReply();
|
||||
this.appEvents.trigger('composer:typed-reply');
|
||||
}
|
||||
},
|
||||
|
||||
// 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
|
||||
|
||||
|
@ -439,13 +473,10 @@ export default Ember.Controller.extend({
|
|||
this.set('scopedCategoryId', opts.categoryId);
|
||||
}
|
||||
|
||||
const composerMessages = this.get('controllers.composer-messages'),
|
||||
self = this;
|
||||
|
||||
const self = this;
|
||||
let composerModel = this.get('model');
|
||||
|
||||
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 (composerModel &&
|
||||
|
@ -493,6 +524,8 @@ export default Ember.Controller.extend({
|
|||
|
||||
// Given a potential instance and options, set the model for this composer.
|
||||
_setModel(composerModel, opts) {
|
||||
this.set('linkLookup', null);
|
||||
|
||||
if (opts.draft) {
|
||||
composerModel = loadDraft(this.store, opts);
|
||||
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) {
|
||||
this.set('model.reply', opts.topicBody);
|
||||
}
|
||||
|
||||
this.get('controllers.composer-messages').queryFor(composerModel);
|
||||
},
|
||||
|
||||
// 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({
|
||||
needs: ['group'],
|
||||
export default Ember.Controller.extend({
|
||||
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: {
|
||||
loadMore() {
|
||||
removeMember(user) {
|
||||
this.get('model').removeMember(user);
|
||||
},
|
||||
|
||||
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);
|
||||
});
|
||||
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)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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({
|
||||
counts: null,
|
||||
showing: 'posts',
|
||||
showing: 'members',
|
||||
|
||||
@observes('counts')
|
||||
countsChanged() {
|
||||
|
@ -35,10 +35,10 @@ export default Ember.Controller.extend({
|
|||
},
|
||||
|
||||
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: 'mentions' }),
|
||||
Tab.create({ name: 'members' }),
|
||||
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
|
||||
// page which is wrong.
|
||||
emailOrUsername: null,
|
||||
hasCustomMessage: false,
|
||||
customMessage: null,
|
||||
inviteIcon: "envelope",
|
||||
|
||||
isAdmin: function(){
|
||||
|
@ -27,6 +29,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'),
|
||||
|
||||
disabledCopyLink: function() {
|
||||
if (this.get('hasCustomMessage')) return true;
|
||||
if (this.get('model.saving')) return true;
|
||||
if (Ember.isEmpty(this.get('emailOrUsername'))) return true;
|
||||
const emailOrUsername = this.get('emailOrUsername').trim();
|
||||
|
@ -37,7 +40,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
// 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;
|
||||
return false;
|
||||
}.property('emailOrUsername', 'model.saving', 'isPrivateTopic', 'model.groupNames'),
|
||||
}.property('emailOrUsername', 'model.saving', 'isPrivateTopic', 'model.groupNames', 'hasCustomMessage'),
|
||||
|
||||
buttonTitle: function() {
|
||||
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');
|
||||
}.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.
|
||||
inviteInstructions: function() {
|
||||
if (Discourse.SiteSettings.enable_sso || !Discourse.SiteSettings.enable_local_logins) {
|
||||
|
@ -102,11 +109,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
}
|
||||
}.property('isMessage', 'invitingToTopic', 'emailOrUsername'),
|
||||
|
||||
// Instructional text for the group selection.
|
||||
groupInstructions: function() {
|
||||
return this.get('isPrivateTopic') ?
|
||||
I18n.t('topic.automatically_add_to_groups_required') :
|
||||
I18n.t('topic.automatically_add_to_groups_optional');
|
||||
showGroupsClass: function() {
|
||||
return this.get('isPrivateTopic') ? 'required' : 'optional';
|
||||
}.property('isPrivateTopic'),
|
||||
|
||||
groupFinder(term) {
|
||||
|
@ -136,9 +140,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
'topic.invite_private.email_or_username_placeholder';
|
||||
}.property(),
|
||||
|
||||
customMessagePlaceholder: function() {
|
||||
return I18n.t('invite.custom_message_placeholder');
|
||||
}.property(),
|
||||
|
||||
// Reset the modal to allow a new user to be invited.
|
||||
reset() {
|
||||
this.set('emailOrUsername', null);
|
||||
this.set('hasCustomMessage', false);
|
||||
this.set('customMessage', null);
|
||||
this.get('model').setProperties({
|
||||
groupNames: null,
|
||||
error: false,
|
||||
|
@ -147,7 +157,6 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
inviteLink: null
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
createInvite() {
|
||||
|
@ -162,7 +171,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
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 });
|
||||
if (!this.get('invitingToTopic')) {
|
||||
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 });
|
||||
});
|
||||
},
|
||||
|
||||
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: {
|
||||
|
||||
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() {
|
||||
this.set('saved', false);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||
import { extractError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, BufferedContent, {
|
||||
|
||||
|
@ -14,11 +15,15 @@ export default Ember.Controller.extend(ModalFunctionality, BufferedContent, {
|
|||
performRename() {
|
||||
const tag = this.get('model'),
|
||||
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.transitionToRoute('tags.show', tag.get('id'));
|
||||
}).catch(function() {
|
||||
self.flash(I18n.t('generic_error'), 'error');
|
||||
if (result.responseJson.tag) {
|
||||
self.transitionToRoute('tags.show', result.responseJson.tag.id);
|
||||
} 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'],
|
||||
|
||||
title: Ember.computed.alias('controllers.topic.model.title'),
|
||||
canReplyAsNewTopic: Ember.computed.alias('controllers.topic.model.details.can_reply_as_new_topic'),
|
||||
|
||||
@computed('type', 'postNumber')
|
||||
shareTitle(type, postNumber) {
|
||||
|
@ -29,6 +30,15 @@ export default Ember.Controller.extend({
|
|||
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) {
|
||||
var url = source.generateUrl(this.get('link'), this.get('title'));
|
||||
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({
|
||||
sortProperties: ['count:desc', 'id'],
|
||||
|
||||
sortedTags: Ember.computed.sort('model', 'sortProperties'),
|
||||
canAdminTags: Ember.computed.alias("currentUser.staff"),
|
||||
|
||||
actions: {
|
||||
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';
|
||||
|
||||
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
needs: ['modal', 'composer', 'quote-button', 'topic-progress', 'application'],
|
||||
needs: ['modal', 'composer', 'quote-button', 'application'],
|
||||
multiSelect: false,
|
||||
allPostsSelected: false,
|
||||
editingTopic: false,
|
||||
selectedPosts: null,
|
||||
selectedReplies: null,
|
||||
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,
|
||||
enteredIndex: null,
|
||||
retrying: false,
|
||||
adminMenuVisible: false,
|
||||
userTriggeredProgress: null,
|
||||
_progressIndex: null,
|
||||
|
||||
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
|
||||
isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"),
|
||||
topicDelegated: [
|
||||
'toggleMultiSelect',
|
||||
'deleteTopic',
|
||||
'recoverTopic',
|
||||
'toggleClosed',
|
||||
'showAutoClose',
|
||||
'showFeatureTopic',
|
||||
'showChangeTimestamp',
|
||||
'toggleArchived',
|
||||
'toggleVisibility',
|
||||
'convertToPublicTopic',
|
||||
'convertToPrivateMessage',
|
||||
'jumpTop',
|
||||
'jumpToPost',
|
||||
'jumpToIndex',
|
||||
'jumpBottom',
|
||||
'replyToPost'
|
||||
],
|
||||
|
||||
_titleChanged: function() {
|
||||
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);
|
||||
},
|
||||
|
||||
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.
|
||||
topVisibleChanged(event) {
|
||||
const { post, refresh } = event;
|
||||
|
||||
if (!post) { return; }
|
||||
|
||||
const postStream = this.get('model.postStream');
|
||||
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 (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) {
|
||||
const { post, refresh } = event;
|
||||
|
||||
const postStream = this.get('model.postStream');
|
||||
const lastLoadedPost = postStream.get('posts.lastObject');
|
||||
|
||||
this.set('controllers.topic-progress.progressPosition', postStream.progressIndexOfPost(post));
|
||||
|
||||
if (lastLoadedPost && lastLoadedPost === post && postStream.get('canAppendMore')) {
|
||||
postStream.appendMore().then(() => refresh());
|
||||
// show loading stuff
|
||||
|
@ -220,14 +256,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
return this.get('model.details').removeAllowedUser(user);
|
||||
},
|
||||
|
||||
showTopicAdminMenu() {
|
||||
this.set('adminMenuVisible', true);
|
||||
},
|
||||
|
||||
hideTopicAdminMenu() {
|
||||
this.set('adminMenuVisible', false);
|
||||
},
|
||||
|
||||
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() {
|
||||
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() {
|
||||
|
@ -546,10 +586,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
},
|
||||
|
||||
replyAsNewTopic(post) {
|
||||
const composerController = this.get('controllers.composer'),
|
||||
quoteController = this.get('controllers.quote-button'),
|
||||
quotedText = Quote.build(quoteController.get('post'), quoteController.get('buffer')),
|
||||
self = this;
|
||||
const composerController = this.get('controllers.composer');
|
||||
const quoteController = this.get('controllers.quote-button');
|
||||
post = post || quoteController.get('post');
|
||||
const quotedText = Quote.build(post, quoteController.get('buffer'));
|
||||
|
||||
quoteController.deselectText();
|
||||
|
||||
|
@ -561,7 +601,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
return Em.isEmpty(quotedText) ? "" : quotedText;
|
||||
}).then(q => {
|
||||
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});
|
||||
});
|
||||
},
|
||||
|
@ -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() {
|
||||
this.send('togglePinnedForUser');
|
||||
},
|
||||
|
@ -792,27 +851,23 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
const postStream = topic.get("postStream");
|
||||
|
||||
if (topic.get('id') === topicId) {
|
||||
|
||||
// TODO identity map for postNumber
|
||||
postStream.get('posts').forEach(post => {
|
||||
if (!post.read && postNumbers.indexOf(post.post_number) !== -1) {
|
||||
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 &&
|
||||
this.currentUser &&
|
||||
this.currentUser.automatically_unpin_topics) {
|
||||
|
||||
// automatically unpin topics when the user reaches the bottom
|
||||
const max = _.max(postNumbers);
|
||||
if (topic.get("pinned") && max >= topic.get("highest_post_number")) {
|
||||
Em.run.next(() => topic.clearPin());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -29,6 +29,11 @@ export default Ember.Controller.extend({
|
|||
linkWebsite: Em.computed.not('user.isBasic'),
|
||||
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')
|
||||
publicUserFields() {
|
||||
const siteUserFields = this.site.get('user_fields');
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Invite from 'discourse/models/invite';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
// This controller handles actions related to a user's invitations
|
||||
export default Ember.Controller.extend({
|
||||
|
@ -10,6 +11,7 @@ export default Ember.Controller.extend({
|
|||
invitesCount: null,
|
||||
canLoadMore: true,
|
||||
invitesLoading: false,
|
||||
reinvitedAll: false,
|
||||
|
||||
init: function() {
|
||||
this._super();
|
||||
|
@ -32,6 +34,10 @@ export default Ember.Controller.extend({
|
|||
|
||||
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
|
||||
|
||||
|
@ -87,6 +93,13 @@ export default Ember.Controller.extend({
|
|||
return false;
|
||||
},
|
||||
|
||||
reinviteAll() {
|
||||
const self = this;
|
||||
Invite.reinviteAll().then(function() {
|
||||
self.set('reinvitedAll', true);
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
var self = this;
|
||||
var model = self.get('model');
|
||||
|
|
|
@ -41,7 +41,12 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
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) {
|
||||
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 DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
// Use the message bus for live reloading of components for faster development.
|
||||
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
|
||||
messageBus.subscribe("/file-change", function(data) {
|
||||
if (Handlebars.compile && !Ember.TEMPLATES.empty) {
|
||||
|
|
|
@ -55,7 +55,12 @@ function shortDateNoYear(date) {
|
|||
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"));
|
||||
}
|
||||
|
||||
|
@ -120,47 +125,46 @@ export function autoUpdatingRelativeAge(date,options) {
|
|||
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 distance = Math.round((new Date() - date) / 1000);
|
||||
const distanceInMinutes = Math.round(distance / 60.0);
|
||||
|
||||
let formatted;
|
||||
const t = function(key,opts){
|
||||
return I18n.t("dates." + format + "." + key, opts);
|
||||
const t = function(key, opts) {
|
||||
const result = I18n.t("dates." + format + "." + key, opts);
|
||||
return (ageOpts && ageOpts.addAgo) ? wrapAgo(result) : result;
|
||||
};
|
||||
|
||||
switch(true){
|
||||
|
||||
case(distanceInMinutes < 1):
|
||||
formatted = t("less_than_x_minutes", {count: 1});
|
||||
break;
|
||||
case(distanceInMinutes >= 1 && distanceInMinutes <= 44):
|
||||
formatted = t("x_minutes", {count: distanceInMinutes});
|
||||
break;
|
||||
case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
|
||||
formatted = t("about_x_hours", {count: 1});
|
||||
break;
|
||||
case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
|
||||
formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
|
||||
break;
|
||||
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()) {
|
||||
switch(true) {
|
||||
case(distanceInMinutes < 1):
|
||||
formatted = t("less_than_x_minutes", {count: 1});
|
||||
break;
|
||||
case(distanceInMinutes >= 1 && distanceInMinutes <= 44):
|
||||
formatted = t("x_minutes", {count: distanceInMinutes});
|
||||
break;
|
||||
case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
|
||||
formatted = t("about_x_hours", {count: 1});
|
||||
break;
|
||||
case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
|
||||
formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
|
||||
break;
|
||||
case(Discourse.SiteSettings.relative_date_duration === 0 && distanceInMinutes <= 525599):
|
||||
formatted = shortDateNoYear(date);
|
||||
} else {
|
||||
formatted = tinyDateYear(date);
|
||||
}
|
||||
break;
|
||||
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:
|
||||
formatted = (ageOpts.defaultFormat || smartShortDate)(date);
|
||||
break;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
|
@ -199,7 +203,7 @@ function relativeAgeMediumSpan(distance, leaveAgo) {
|
|||
formatted = t("x_days", {count: Math.round((distanceInMinutes - 720.0) / 1440.0)});
|
||||
break;
|
||||
}
|
||||
return formatted || '&mdash';
|
||||
return formatted || '—';
|
||||
}
|
||||
|
||||
function relativeAgeMedium(date, options) {
|
||||
|
@ -219,11 +223,7 @@ function relativeAgeMedium(date, options) {
|
|||
if (distance < oneMinuteAgo) {
|
||||
displayDate = I18n.t("now");
|
||||
} else if (distance > fiveDaysAgo) {
|
||||
if ((new Date()).getFullYear() !== date.getFullYear()) {
|
||||
displayDate = shortDate(date);
|
||||
} else {
|
||||
displayDate = shortDateNoYear(date);
|
||||
}
|
||||
displayDate = smartShortDate(date, shortDate);
|
||||
} else {
|
||||
displayDate = relativeAgeMediumSpan(distance, leaveAgo);
|
||||
}
|
||||
|
@ -239,7 +239,7 @@ export function relativeAge(date, options) {
|
|||
options = options || {};
|
||||
const format = options.format || "tiny";
|
||||
|
||||
if(format === "tiny") {
|
||||
if (format === "tiny") {
|
||||
return relativeAgeTiny(date, options);
|
||||
} else if (format === "medium") {
|
||||
return relativeAgeMedium(date, options);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import DiscourseURL from 'discourse/lib/url';
|
||||
import Composer from 'discourse/models/composer';
|
||||
import { scrollTopFor } from 'discourse/lib/offset-calculator';
|
||||
|
||||
const bindings = {
|
||||
'!': {postAction: 'showFlags'},
|
||||
|
@ -116,7 +117,7 @@ export default {
|
|||
|
||||
_jumpTo(direction) {
|
||||
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() {
|
||||
this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true});
|
||||
this.appEvents.trigger('topic-progress:keyboard-trigger', { type: 'jump' });
|
||||
},
|
||||
|
||||
toggleSearch(event) {
|
||||
|
@ -298,12 +299,19 @@ export default {
|
|||
|
||||
if ($article.is('.topic-post')) {
|
||||
$('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) {
|
||||
// Try to keep the article on screen
|
||||
const pos = $article.offset();
|
||||
|
|
29
app/assets/javascripts/discourse/lib/link-lookup.js.es6
Normal file
29
app/assets/javascripts/discourse/lib/link-lookup.js.es6
Normal file
|
@ -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
|
||||
// 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
|
||||
|
@ -16,75 +18,66 @@
|
|||
// 1. onbeforeunload ensure we are scrolled to the right spot
|
||||
// 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.options = options || {};
|
||||
};
|
||||
|
||||
LockOn.prototype.elementTop = function() {
|
||||
var offsetCalculator = this.options.offsetCalculator,
|
||||
selected = $(this.selector);
|
||||
this.offsetTop = null;
|
||||
}
|
||||
|
||||
elementTop() {
|
||||
const selected = $(this.selector);
|
||||
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() {
|
||||
var self = this,
|
||||
previousTop = this.elementTop(),
|
||||
startedAt = new Date().getTime(),
|
||||
i = 0;
|
||||
clearLock(interval) {
|
||||
$('body,html').off(SCROLL_EVENTS);
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
lock() {
|
||||
let previousTop = this.elementTop();
|
||||
const startedAt = new Date().getTime();
|
||||
|
||||
$(window).scrollTop(previousTop);
|
||||
|
||||
var within = function(threshold,x,y) {
|
||||
return Math.abs(x-y) < threshold;
|
||||
};
|
||||
let i = 0;
|
||||
|
||||
var interval = setInterval(function() {
|
||||
const interval = setInterval(() => {
|
||||
i = i + 1;
|
||||
|
||||
var top = self.elementTop(),
|
||||
scrollTop = $(window).scrollTop();
|
||||
let top = this.elementTop();
|
||||
const scrollTop = $(window).scrollTop();
|
||||
|
||||
if (typeof(top) === "undefined") {
|
||||
$('body,html').off(scrollEvents);
|
||||
clearInterval(interval);
|
||||
return;
|
||||
if (typeof(top) === "undefined" || isNaN(top)) {
|
||||
return this.clearLock(interval);
|
||||
}
|
||||
|
||||
if (!within(4, top, previousTop) || !within(4, scrollTop, top)) {
|
||||
$(window).scrollTop(top);
|
||||
// animating = true;
|
||||
// $('html,body').animate({scrollTop: parseInt(top,10)+'px'}, 200, 'swing', function(){
|
||||
// animating = false;
|
||||
// });
|
||||
previousTop = top;
|
||||
}
|
||||
|
||||
// We commit suicide after 3s just to clean up
|
||||
var nowTime = new Date().getTime();
|
||||
const nowTime = new Date().getTime();
|
||||
if (nowTime - startedAt > 1000) {
|
||||
$('body,html').off(scrollEvents);
|
||||
clearInterval(interval);
|
||||
return this.clearLock(interval);
|
||||
}
|
||||
|
||||
}, 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") {
|
||||
$('body,html').off(scrollEvents);
|
||||
clearInterval(interval);
|
||||
this.clearLock(interval);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
exports.LockOn = LockOn;
|
||||
|
||||
})(window);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,24 @@
|
|||
export default {
|
||||
const NotificationLevels = {
|
||||
WATCHING: 3,
|
||||
TRACKING: 2,
|
||||
REGULAR: 1,
|
||||
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 { h } from 'virtual-dom';
|
||||
import { addFlagProperty } from 'discourse/components/site-header';
|
||||
import { addPopupMenuOptionsCallback } from 'discourse/controllers/composer';
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
|
@ -224,6 +225,26 @@ class PluginApi {
|
|||
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.
|
||||
* 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;
|
||||
const rewrites = [];
|
||||
|
||||
|
@ -14,14 +16,6 @@ const DiscourseURL = Ember.Object.extend({
|
|||
// Jumps to a particular post in the stream
|
||||
jumpToPost(postNumber, opts) {
|
||||
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', () => {
|
||||
if (postNumber === 1) {
|
||||
|
@ -29,15 +23,14 @@ const DiscourseURL = Ember.Object.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
const lockon = new LockOn(holderId, {offsetCalculator: offset});
|
||||
const lockon = new LockOn(holderId);
|
||||
const holder = $(holderId);
|
||||
|
||||
if (holder.length > 0 && opts && opts.skipIfOnScreen){
|
||||
// if we are on screen skip
|
||||
const elementTop = lockon.elementTop(),
|
||||
scrollTop = $(window).scrollTop(),
|
||||
windowHeight = $(window).height()-offset(),
|
||||
height = holder.height();
|
||||
const elementTop = lockon.elementTop();
|
||||
const scrollTop = $(window).scrollTop();
|
||||
const windowHeight = $(window).height() - offsetCalculator();
|
||||
const height = holder.height();
|
||||
|
||||
if (elementTop > scrollTop && (elementTop + height) < (scrollTop + windowHeight)) {
|
||||
return;
|
||||
|
@ -107,6 +100,8 @@ const DiscourseURL = Ember.Object.extend({
|
|||
keep the history intact.
|
||||
**/
|
||||
routeTo(path, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
if (Em.isEmpty(path)) { return; }
|
||||
|
||||
if (Discourse.get('requiresRefresh')) {
|
||||
|
@ -150,12 +145,12 @@ const DiscourseURL = Ember.Object.extend({
|
|||
|
||||
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
|
||||
Em.run.scheduleOnce('afterRender', Discourse.Route, 'cleanDOM');
|
||||
|
||||
// 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 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);
|
||||
},
|
||||
|
||||
rewrite: function(regexp, replacement) {
|
||||
rewrites.push({ regexp: regexp, replacement: replacement });
|
||||
rewrite(regexp, replacement) {
|
||||
rewrites.push({ regexp, replacement });
|
||||
},
|
||||
|
||||
redirectTo: function(url) {
|
||||
redirectTo(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
|
||||
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.
|
||||
|
||||
@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 newTopicId = newMatches ? newMatches[2] : null;
|
||||
|
||||
|
@ -232,14 +221,9 @@ const DiscourseURL = Ember.Object.extend({
|
|||
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);
|
||||
}).then(() => {
|
||||
DiscourseURL.jumpToPost(closest, {skipIfOnScreen: true});
|
||||
DiscourseURL.jumpToPost(closest, {skipIfOnScreen: routeOpts.skipIfOnScreen});
|
||||
});
|
||||
|
||||
// Abort routing, we have replaced our state.
|
||||
|
|
|
@ -91,7 +91,7 @@ export default function userSearch(options) {
|
|||
|
||||
return new Ember.RSVP.Promise(function(resolve) {
|
||||
// TODO site setting for allowed regex in username
|
||||
if (term.match(/[^a-zA-Z0-9_\.\-]/)) {
|
||||
if (term.match(/[^\w\.\-]/)) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export default Ember.Mixin.create({
|
||||
init() {
|
||||
this._super();
|
||||
(this.get('delegated') || []).forEach(m => this.set(m, m));
|
||||
},
|
||||
});
|
38
app/assets/javascripts/discourse/mixins/docking.js.es6
Normal file
38
app/assets/javascripts/discourse/mixins/docking.js.es6
Normal file
|
@ -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({
|
||||
action: Composer.CREATE_TOPIC,
|
||||
topicTitle,
|
||||
topicBody,
|
||||
topicCategoryId,
|
||||
topicCategory,
|
||||
topicTags,
|
||||
draftKey: controller.get('model.draft_key'),
|
||||
draftSequence: controller.get('model.draft_sequence')
|
||||
});
|
||||
|
|
|
@ -86,7 +86,9 @@ const Category = RestModel.extend({
|
|||
allow_badges: this.get('allow_badges'),
|
||||
custom_fields: this.get('custom_fields'),
|
||||
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'
|
||||
});
|
||||
|
|
|
@ -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({
|
||||
|
||||
rescind() {
|
||||
|
@ -9,11 +11,13 @@ const Invite = Discourse.Model.extend({
|
|||
},
|
||||
|
||||
reinvite() {
|
||||
Discourse.ajax('/invites/reinvite', {
|
||||
const self = this;
|
||||
return Discourse.ajax('/invites/reinvite', {
|
||||
type: 'POST',
|
||||
data: { email: this.get('email') }
|
||||
});
|
||||
this.set('reinvited', true);
|
||||
}).then(function() {
|
||||
self.set('reinvited', true);
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -48,6 +52,10 @@ Invite.reopenClass({
|
|||
findInvitedCount(user) {
|
||||
if (!user) { return Em.RSVP.resolve(); }
|
||||
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,
|
||||
stagingPost: null,
|
||||
postsWithPlaceholders: null,
|
||||
timelineLookup: null,
|
||||
|
||||
init() {
|
||||
this._identityMap = {};
|
||||
|
@ -33,6 +34,7 @@ export default RestModel.extend({
|
|||
loadingBelow: false,
|
||||
loadingFilter: false,
|
||||
stagingPost: false,
|
||||
timelineLookup: []
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -217,7 +219,7 @@ export default RestModel.extend({
|
|||
// Request a topicView
|
||||
return loadTopicView(topic, opts).then(json => {
|
||||
this.updateFromJson(json.post_stream);
|
||||
this.setProperties({ loadingFilter: false, loaded: true });
|
||||
this.setProperties({ loadingFilter: false, timelineLookup: json.timeline_lookup, loaded: true });
|
||||
}).catch(result => {
|
||||
this.errorLoading(result);
|
||||
throw result;
|
||||
|
@ -612,6 +614,27 @@ export default RestModel.extend({
|
|||
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
|
||||
findPostIdForPostNumber(postNumber) {
|
||||
const stream = this.get('stream'),
|
||||
|
@ -673,6 +696,7 @@ export default RestModel.extend({
|
|||
const postNumber = post.get('post_number');
|
||||
if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) {
|
||||
this.set('topic.highest_post_number', postNumber);
|
||||
this.set('topic.last_posted_at', post.get('created_at'));
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
|
|
40
app/assets/javascripts/discourse/models/tag-group.js.es6
Normal file
40
app/assets/javascripts/discourse/models/tag-group.js.es6
Normal file
|
@ -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.
|
||||
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';
|
||||
|
||||
const TopicDetails = RestModel.extend({
|
||||
|
@ -35,20 +36,21 @@ const TopicDetails = RestModel.extend({
|
|||
},
|
||||
|
||||
notificationReasonText: function() {
|
||||
var level = this.get('notification_level');
|
||||
if(typeof level !== 'number'){
|
||||
level = 1;
|
||||
let level = this.get('notification_level');
|
||||
if (typeof level !== 'number') { 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 (typeof this.get('notifications_reason_id') === 'number') {
|
||||
var tmp = localeString + "_" + this.get('notifications_reason_id');
|
||||
// some sane protection for missing translations of edge cases
|
||||
if(I18n.lookup(tmp)){
|
||||
localeString = tmp;
|
||||
}
|
||||
if (Discourse.User.currentProp('mailing_list_mode') && level > NotificationLevels.MUTED) {
|
||||
return I18n.t("topic.notifications.reasons.mailing_list_mode");
|
||||
} else {
|
||||
return I18n.t(localeString, { username: Discourse.User.currentProp('username_lower') });
|
||||
}
|
||||
return I18n.t(localeString, { username: Discourse.User.currentProp('username_lower') });
|
||||
}.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", {
|
||||
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'),
|
||||
|
||||
watchingTopicsPath: url('/latest?state=watching'),
|
||||
|
||||
@computed("username")
|
||||
username_lower(username) {
|
||||
return username.toLowerCase();
|
||||
|
@ -321,10 +323,10 @@ const User = RestModel.extend({
|
|||
Discourse.SiteSettings['newuser_max_' + type + 's'] > 0;
|
||||
},
|
||||
|
||||
createInvite(email, group_names) {
|
||||
createInvite(email, group_names, custom_message) {
|
||||
return Discourse.ajax('/invites', {
|
||||
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.route('members');
|
||||
this.route('posts');
|
||||
this.route('topics');
|
||||
this.route('mentions');
|
||||
this.route('members');
|
||||
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.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'});
|
||||
},
|
||||
|
||||
createNewTopicViaParams(title, body, category_id, category) {
|
||||
this.openComposerWithTopicParams(this.controllerFor('discovery/topics'), title, body, category_id, category);
|
||||
createNewTopicViaParams(title, body, category_id, category, tags) {
|
||||
this.openComposerWithTopicParams(this.controllerFor('discovery/topics'), title, body, category_id, category, tags);
|
||||
},
|
||||
|
||||
createNewMessageViaParams(username, title, body) {
|
||||
|
|
|
@ -1,24 +1,11 @@
|
|||
export function buildIndex(type) {
|
||||
return Discourse.Route.extend({
|
||||
type,
|
||||
export default Discourse.Route.extend({
|
||||
model() {
|
||||
return this.modelFor("group");
|
||||
},
|
||||
|
||||
model() {
|
||||
return this.modelFor("group").findPosts({ type });
|
||||
},
|
||||
|
||||
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');
|
||||
setupController(controller, model) {
|
||||
this.controllerFor("group").set("showing", "members");
|
||||
controller.set("model", model);
|
||||
model.findMembers();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
export default Discourse.Route.extend({
|
||||
model() {
|
||||
return this.modelFor("group");
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this.controllerFor("group").set("showing", "members");
|
||||
controller.set("model", model);
|
||||
model.findMembers();
|
||||
beforeModel: function() {
|
||||
this.transitionTo("group.index");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
24
app/assets/javascripts/discourse/routes/group-posts.js.es6
Normal file
24
app/assets/javascripts/discourse/routes/group-posts.js.es6
Normal file
|
@ -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')) {
|
||||
// User can create topic
|
||||
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() {
|
||||
this.controllerFor("application").set("showFooter", true);
|
||||
return true;
|
||||
},
|
||||
|
||||
showTagGroups() {
|
||||
this.transitionTo('tagGroups');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ export default Discourse.Route.extend({
|
|||
topic = this.modelFor('topic'),
|
||||
postStream = topic.get('postStream'),
|
||||
topicController = this.controllerFor('topic'),
|
||||
topicProgressController = this.controllerFor('topic-progress'),
|
||||
composerController = this.controllerFor('composer');
|
||||
|
||||
// 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.
|
||||
|
||||
// The post we requested might not exist. Let's find the closest post
|
||||
const closestPost = postStream.closestPostForPostNumber(params.nearPost || 1),
|
||||
closest = closestPost.get('post_number'),
|
||||
progress = postStream.progressIndexOfPost(closestPost);
|
||||
const closestPost = postStream.closestPostForPostNumber(params.nearPost || 1);
|
||||
const closest = closestPost.get('post_number');
|
||||
|
||||
topicController.setProperties({
|
||||
'model.currentPost': closest,
|
||||
enteredIndex: postStream.get('stream').indexOf(closestPost.get('id')),
|
||||
enteredAt: new Date().getTime().toString(),
|
||||
});
|
||||
|
||||
topicProgressController.setProperties({
|
||||
progressPosition: progress,
|
||||
expanded: false
|
||||
});
|
||||
|
||||
// Highlight our post after the next render
|
||||
Ember.run.scheduleOnce('afterRender', function() {
|
||||
self.appEvents.trigger('post:highlight', closest);
|
||||
|
|
|
@ -210,8 +210,6 @@ const TopicRoute = Discourse.Route.extend({
|
|||
this.topicTrackingState.trackIncoming('all');
|
||||
controller.subscribe();
|
||||
|
||||
this.controllerFor('topic-progress').set('model', model);
|
||||
|
||||
// We reset screen tracking every time a topic is entered
|
||||
this.screenTrack.start(model.get('id'), controller);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
|
||||
|
||||
const LOGS_NOTICE_KEY = "logs-notice-text";
|
||||
|
||||
|
@ -28,6 +28,7 @@ const LogsNotice = Ember.Object.extend({
|
|||
|
||||
this.set('text',
|
||||
I18n.t(`logs_error_rate_notice.${translationKey}`, {
|
||||
relativeAge: autoUpdatingRelativeAge(new Date),
|
||||
timestamp: moment().format("YYYY-MM-DD H:mm:ss"),
|
||||
siteSettingRate: I18n.t('logs_error_rate_notice.rate', { count: siteSettingLimit, 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}}
|
||||
{{#popup-menu visible=optionsVisible hide="hideOptions" title="composer.options"}}
|
||||
<li>
|
||||
{{d-button action="toggleWhisper" icon="eye-slash" label="composer.toggle_whisper"}}
|
||||
</li>
|
||||
{{#each popupMenuOptions as |option|}}
|
||||
{{#if option.condition}}
|
||||
<li>
|
||||
{{d-button action=option.action icon=option.icon label=option.label}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/popup-menu}}
|
||||
{{/if}}
|
||||
|
||||
{{render "composer-messages"}}
|
||||
<div class='control'>
|
||||
{{composer-messages composer=model messageCount=messageCount addLinkLookup="addLinkLookup"}}
|
||||
|
||||
<div class='control'>
|
||||
{{#if site.mobileView}}
|
||||
<a href class='toggle-toolbar' {{action "toggleToolbar" bubbles=false}}></a>
|
||||
{{/if}}
|
||||
|
@ -86,19 +90,21 @@
|
|||
composer=model
|
||||
lastValidatedAt=lastValidatedAt
|
||||
canWhisper=canWhisper
|
||||
popupMenuOptions=popupMenuOptions
|
||||
draftStatus=model.draftStatus
|
||||
isUploading=isUploading
|
||||
groupsMentioned="groupsMentioned"
|
||||
importQuote="importQuote"
|
||||
showOptions="showOptions"
|
||||
showToolbar=showToolbar
|
||||
showUploadSelector="showUploadSelector"}}
|
||||
showUploadSelector="showUploadSelector"
|
||||
afterRefresh="afterRefresh"}}
|
||||
|
||||
{{#if currentUser}}
|
||||
<div class='submit-panel'>
|
||||
{{plugin-outlet "composer-fields-below"}}
|
||||
{{#if canEditTags}}
|
||||
{{tag-chooser tags=model.tags tabIndex="4"}}
|
||||
{{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}}
|
||||
{{/if}}
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
{{{body}}}
|
||||
<a href {{action "closeMessage"}} class='close'>{{fa-icon "times"}}</a>
|
||||
{{{message.body}}}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
<a href {{action "closeMessage" this}} class='close'><i class='fa fa-close'></i></a>
|
||||
{{{body}}}
|
||||
<a href {{action "closeMessage"}} class='close'>{{fa-icon "close"}}</a>
|
||||
{{{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>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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">
|
||||
<td class="label"><label for='new-account-username'>{{i18n 'user.username.title'}}</label></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"}}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
{{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="topic-template"}}
|
||||
{{#if siteSettings.tagging_enabled}}
|
||||
{{edit-category-tab panels=panels selectedTab=selectedTab tab="tags"}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
|
||||
<div class="modal-body">
|
||||
|
|
|
@ -19,9 +19,15 @@
|
|||
{{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}}
|
||||
{{/if}}
|
||||
{{#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"}}
|
||||
{{/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}}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
|
@ -14,6 +14,12 @@
|
|||
{{share-source source=s title=title action="share"}}
|
||||
{{/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'>
|
||||
<a href {{action "close"}} aria-label='{{i18n 'share.close'}}' title='{{i18n 'share.close'}}'>{{fa-icon "close"}}</a>
|
||||
</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 a new issue