mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 18:01:25 +08:00
REFACTOR: Composer messages to use new ember idioms
This commit is contained in:
@ -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:${templateName}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
this.$().show();
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
closeMessage() {
|
||||||
|
this.sendAction('closeMessage', this.get('message'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,163 @@
|
|||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
_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));
|
||||||
|
},
|
||||||
|
|
||||||
|
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})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_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: 'composer/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 = { composerAction: 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 => {
|
||||||
|
this.set('checkedMessages', true);
|
||||||
|
messages.forEach(msg => msg.wait_for_typing ? queuedForTyping.addObject(msg) : this.send('popup', msg));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -1,85 +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 args = { composerAction: 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');
|
|
||||||
this.store.findAll('composer-message', args).then(messages => {
|
|
||||||
this.set('checkedMessages', true);
|
|
||||||
messages.forEach(msg => msg.wait_for_typing ? queuedForTyping.addObject(msg) : this.send('popup', msg));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -42,16 +42,14 @@ function loadDraft(store, opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
needs: ['modal', 'topic', 'composer-messages', 'application'],
|
needs: ['modal', 'topic', 'application'],
|
||||||
replyAsNewTopicDraft: Em.computed.equal('model.draftKey', Composer.REPLY_AS_NEW_TOPIC_KEY),
|
replyAsNewTopicDraft: Em.computed.equal('model.draftKey', Composer.REPLY_AS_NEW_TOPIC_KEY),
|
||||||
checkedMessages: false,
|
checkedMessages: false,
|
||||||
|
messageCount: null,
|
||||||
|
|
||||||
showEditReason: false,
|
showEditReason: false,
|
||||||
editReason: null,
|
editReason: null,
|
||||||
scopedCategoryId: null,
|
scopedCategoryId: null,
|
||||||
similarTopics: null,
|
|
||||||
similarTopicsMessage: null,
|
|
||||||
lastSimilaritySearch: null,
|
|
||||||
optionsVisible: false,
|
optionsVisible: false,
|
||||||
lastValidatedAt: null,
|
lastValidatedAt: null,
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
@ -78,10 +76,6 @@ export default Ember.Controller.extend({
|
|||||||
|
|
||||||
topicModel: Ember.computed.alias('controllers.topic.model'),
|
topicModel: Ember.computed.alias('controllers.topic.model'),
|
||||||
|
|
||||||
_initializeSimilar: function() {
|
|
||||||
this.set('similarTopics', []);
|
|
||||||
}.on('init'),
|
|
||||||
|
|
||||||
@computed('model.canEditTitle', 'model.creatingPrivateMessage')
|
@computed('model.canEditTitle', 'model.creatingPrivateMessage')
|
||||||
canEditTags(canEditTitle, creatingPrivateMessage) {
|
canEditTags(canEditTitle, creatingPrivateMessage) {
|
||||||
return !this.site.mobileView &&
|
return !this.site.mobileView &&
|
||||||
@ -183,9 +177,8 @@ export default Ember.Controller.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
hitEsc() {
|
hitEsc() {
|
||||||
const messages = this.get('controllers.composer-messages.model');
|
if ((this.get('messageCount') || 0) > 0) {
|
||||||
if (messages.length) {
|
this.appEvents.trigger('composer-messages:close');
|
||||||
messages.popObject();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,62 +352,14 @@ export default Ember.Controller.extend({
|
|||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Checks to see if a reply has been typed.
|
// Notify the composer messages controller that a reply has been typed. Some
|
||||||
// This is signaled by a keyUp event in a view.
|
// messages only appear after typing.
|
||||||
checkReplyLength() {
|
checkReplyLength() {
|
||||||
if (!Ember.isEmpty('model.reply')) {
|
if (!Ember.isEmpty('model.reply')) {
|
||||||
// Notify the composer messages controller that a reply has been typed. Some
|
this.appEvents.trigger('composer:typed-reply');
|
||||||
// messages only appear after typing.
|
|
||||||
this.get('controllers.composer-messages').typedReply();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fired after a user stops typing.
|
|
||||||
// Considers whether to check for similar topics based on the current composer state.
|
|
||||||
findSimilarTopics() {
|
|
||||||
// We don't care about similar topics unless creating a topic
|
|
||||||
if (!this.get('model.creatingTopic')) { return; }
|
|
||||||
|
|
||||||
let body = this.get('model.reply') || '';
|
|
||||||
const title = this.get('model.title') || '';
|
|
||||||
|
|
||||||
// Ensure the fields are of the minimum length
|
|
||||||
if (body.length < Discourse.SiteSettings.min_body_similar_length) { return; }
|
|
||||||
if (title.length < Discourse.SiteSettings.min_title_similar_length) { return; }
|
|
||||||
|
|
||||||
// TODO pass the 200 in from somewhere
|
|
||||||
body = body.substr(0, 200);
|
|
||||||
|
|
||||||
// Done search over and over
|
|
||||||
if ((title + body) === this.get('lastSimilaritySearch')) { return; }
|
|
||||||
this.set('lastSimilaritySearch', title + body);
|
|
||||||
|
|
||||||
const messageController = this.get('controllers.composer-messages'),
|
|
||||||
similarTopics = this.get('similarTopics');
|
|
||||||
|
|
||||||
let message = this.get('similarTopicsMessage');
|
|
||||||
if (!message) {
|
|
||||||
message = this.store.createRecord('composer-message', {
|
|
||||||
id: 'similar_topics',
|
|
||||||
templateName: 'composer/similar-topics',
|
|
||||||
extraClass: 'similar-topics'
|
|
||||||
});
|
|
||||||
this.set('similarTopicsMessage', message);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.store.find('similar-topic', {title, raw: body}).then(function(newTopics) {
|
|
||||||
similarTopics.clear();
|
|
||||||
similarTopics.pushObjects(newTopics.get('content'));
|
|
||||||
|
|
||||||
if (similarTopics.get('length') > 0) {
|
|
||||||
message.set('similarTopics', similarTopics);
|
|
||||||
messageController.send("popup", message);
|
|
||||||
} else if (message) {
|
|
||||||
messageController.send("hideMessage", message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Open the composer view
|
Open the composer view
|
||||||
|
|
||||||
@ -439,13 +384,10 @@ export default Ember.Controller.extend({
|
|||||||
this.set('scopedCategoryId', opts.categoryId);
|
this.set('scopedCategoryId', opts.categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const composerMessages = this.get('controllers.composer-messages'),
|
const self = this;
|
||||||
self = this;
|
|
||||||
|
|
||||||
let composerModel = this.get('model');
|
let composerModel = this.get('model');
|
||||||
|
|
||||||
this.setProperties({ showEditReason: false, editReason: null });
|
this.setProperties({ showEditReason: false, editReason: null });
|
||||||
composerMessages.reset();
|
|
||||||
|
|
||||||
// If we want a different draft than the current composer, close it and clear our model.
|
// If we want a different draft than the current composer, close it and clear our model.
|
||||||
if (composerModel &&
|
if (composerModel &&
|
||||||
@ -539,8 +481,6 @@ export default Ember.Controller.extend({
|
|||||||
if (opts.topicBody) {
|
if (opts.topicBody) {
|
||||||
this.set('model.reply', opts.topicBody);
|
this.set('model.reply', opts.topicBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.get('controllers.composer-messages').queryFor(composerModel);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// View a new reply we've made
|
// View a new reply we've made
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
{{#each messages as |message|}}
|
||||||
|
{{composer-message message=message closeMessage="closeMessage"}}
|
||||||
|
{{/each}}
|
@ -9,9 +9,9 @@
|
|||||||
{{/popup-menu}}
|
{{/popup-menu}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{render "composer-messages"}}
|
{{composer-messages composer=model messageCount=messageCount}}
|
||||||
<div class='control'>
|
|
||||||
|
|
||||||
|
<div class='control'>
|
||||||
{{#if site.mobileView}}
|
{{#if site.mobileView}}
|
||||||
<a href class='toggle-toolbar' {{action "toggleToolbar" bubbles=false}}></a>
|
<a href class='toggle-toolbar' {{action "toggleToolbar" bubbles=false}}></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
<a href {{action "closeMessage" this}} class='close'><i class='fa fa-times'></i></a>
|
<a href {{action "closeMessage"}} class='close'>{{fa-icon "times"}}</a>
|
||||||
{{{body}}}
|
{{{message.body}}}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
<a href {{action "closeMessage" this}} class='close'><i class='fa fa-close'></i></a>
|
<a href {{action "closeMessage"}} class='close'>{{fa-icon "close"}}</a>
|
||||||
{{{body}}}
|
{{{message.body}}}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<a href {{action "closeMessage" this}} class='close'>{{fa-icon "close"}}</a>
|
<a href {{action "closeMessage"}} class='close'>{{fa-icon "close"}}</a>
|
||||||
<h3>{{i18n 'composer.similar_topics'}}</h3>
|
<h3>{{i18n 'composer.similar_topics'}}</h3>
|
||||||
|
|
||||||
<ul class='topics'>
|
<ul class='topics'>
|
||||||
{{mount-widget widget="search-result-topic" args=(as-hash results=similarTopics)}}
|
{{mount-widget widget="search-result-topic" args=(as-hash results=message.similarTopics)}}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
export default Ember.CollectionView.extend({
|
|
||||||
classNameBindings: [':composer-popup-container', 'hidden'],
|
|
||||||
content: Em.computed.alias('controller.content'),
|
|
||||||
|
|
||||||
hidden: Em.computed.not('controller.controllers.composer.model.viewOpen'),
|
|
||||||
|
|
||||||
itemViewClass: Ember.View.extend({
|
|
||||||
classNames: ['composer-popup', 'hidden'],
|
|
||||||
templateName: Em.computed.alias('content.templateName'),
|
|
||||||
|
|
||||||
_setup: function() {
|
|
||||||
this._super();
|
|
||||||
this.set('context', this.get('content'));
|
|
||||||
|
|
||||||
if (this.get('content.extraClass')) {
|
|
||||||
this.get('classNames').pushObject(this.get('content.extraClass'));
|
|
||||||
}
|
|
||||||
}.on('init'),
|
|
||||||
|
|
||||||
_initCss: function() {
|
|
||||||
this.$().show();
|
|
||||||
}.on('didInsertElement')
|
|
||||||
})
|
|
||||||
});
|
|
@ -64,9 +64,7 @@ const ComposerView = Ember.View.extend({
|
|||||||
Ember.run.cancel(this._lastKeyTimeout);
|
Ember.run.cancel(this._lastKeyTimeout);
|
||||||
this._lastKeyTimeout = Ember.run.later(() => {
|
this._lastKeyTimeout = Ember.run.later(() => {
|
||||||
if (lastKeyUp !== this._lastKeyUp) { return; }
|
if (lastKeyUp !== this._lastKeyUp) { return; }
|
||||||
|
this.appEvents.trigger('composer:find-similar');
|
||||||
// Search for similar topics if the user pauses typing
|
|
||||||
controller.findSimilarTopics();
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ export default function() {
|
|||||||
|
|
||||||
this.get('/admin/plugins', () => response({ plugins: [] }));
|
this.get('/admin/plugins', () => response({ plugins: [] }));
|
||||||
|
|
||||||
this.get('/composer-messages', () => response([]));
|
this.get('/composer_messages', () => response({ composer_messages: [] }));
|
||||||
|
|
||||||
this.get("/latest.json", () => {
|
this.get("/latest.json", () => {
|
||||||
const json = fixturesByUrl['/latest.json'];
|
const json = fixturesByUrl['/latest.json'];
|
||||||
|
Reference in New Issue
Block a user