REFACTOR: Composer messages to use new ember idioms

This commit is contained in:
Robin Ward 2016-06-06 15:23:19 -04:00
parent 6aaa484baa
commit 67303d7679
12 changed files with 205 additions and 189 deletions

View File

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

View File

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

View File

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

View File

@ -42,16 +42,14 @@ function loadDraft(store, opts) {
}
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),
checkedMessages: false,
messageCount: null,
showEditReason: false,
editReason: null,
scopedCategoryId: null,
similarTopics: null,
similarTopicsMessage: null,
lastSimilaritySearch: null,
optionsVisible: false,
lastValidatedAt: null,
isUploading: false,
@ -78,10 +76,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 &&
@ -183,9 +177,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;
}
@ -359,62 +352,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 = 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
@ -439,13 +384,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 &&
@ -539,8 +481,6 @@ export default Ember.Controller.extend({
if (opts.topicBody) {
this.set('model.reply', opts.topicBody);
}
this.get('controllers.composer-messages').queryFor(composerModel);
},
// View a new reply we've made

View File

@ -0,0 +1,3 @@
{{#each messages as |message|}}
{{composer-message message=message closeMessage="closeMessage"}}
{{/each}}

View File

@ -9,9 +9,9 @@
{{/popup-menu}}
{{/if}}
{{render "composer-messages"}}
<div class='control'>
{{composer-messages composer=model messageCount=messageCount}}
<div class='control'>
{{#if site.mobileView}}
<a href class='toggle-toolbar' {{action "toggleToolbar" bubbles=false}}></a>
{{/if}}

View File

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

View File

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

View File

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

View File

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

View File

@ -64,9 +64,7 @@ const ComposerView = Ember.View.extend({
Ember.run.cancel(this._lastKeyTimeout);
this._lastKeyTimeout = Ember.run.later(() => {
if (lastKeyUp !== this._lastKeyUp) { return; }
// Search for similar topics if the user pauses typing
controller.findSimilarTopics();
this.appEvents.trigger('composer:find-similar');
}, 1000);
},

View File

@ -42,7 +42,7 @@ export default function() {
this.get('/admin/plugins', () => response({ plugins: [] }));
this.get('/composer-messages', () => response([]));
this.get('/composer_messages', () => response({ composer_messages: [] }));
this.get("/latest.json", () => {
const json = fixturesByUrl['/latest.json'];