FEATURE: pasting a link into the title of the composer can automatically onebox it and update the title

This commit is contained in:
Neil Lalonde 2016-12-08 16:08:44 -05:00
parent a9acced4ca
commit fbd8e6ed4a
9 changed files with 158 additions and 10 deletions

View File

@ -1,8 +1,11 @@
import computed from 'ember-addons/ember-computed-decorators'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import InputValidation from 'discourse/models/input-validation'; import InputValidation from 'discourse/models/input-validation';
import { load, lookupCache } from 'pretty-text/oneboxer';
import { ajax } from 'discourse/lib/ajax';
export default Ember.Component.extend({ export default Ember.Component.extend({
classNames: ['title-input'], classNames: ['title-input'],
watchForLink: Ember.computed.alias('composer.canEditTopicFeaturedLink'),
didInsertElement() { didInsertElement() {
this._super(); this._super();
@ -26,5 +29,74 @@ export default Ember.Component.extend({
if (reason) { if (reason) {
return InputValidation.create({ failed: true, reason, lastShownAt: lastValidatedAt }); return InputValidation.create({ failed: true, reason, lastShownAt: lastValidatedAt });
} }
},
@observes('composer.titleLength')
_titleChanged() {
if (this.get('composer.titleLength') === 0) { this.set('autoPosted', false); }
if (this.get('autoPosted') || !this.get('watchForLink')) { return; }
if (Ember.testing) {
this._checkForUrl();
} else {
Ember.run.debounce(this, this._checkForUrl, 500);
}
},
@observes('composer.replyLength')
_clearFeaturedLink() {
if (this.get('watchForLink') && this.get('composer.replyLength') === 0) {
this.set('composer.featuredLink', null);
}
},
_checkForUrl() {
if (this.get('isAbsoluteUrl') && (this.get('composer.reply')||"").length === 0) {
// Try to onebox. If success, update post body and title.
this.set('composer.loading', true);
const link = document.createElement('a');
link.href = this.get('composer.title');
let loadOnebox = load(link, false, ajax);
if (loadOnebox && loadOnebox.then) {
loadOnebox.then( () => {
this._updatePost(lookupCache(this.get('composer.title')));
}).finally(() => {
this.set('composer.loading', false);
});
} else {
this._updatePost(loadOnebox);
this.set('composer.loading', false);
}
}
},
_updatePost(html) {
if (html) {
this.set('autoPosted', true);
this.set('composer.featuredLink', this.get('composer.title'));
const $h = $(html),
header = $h.find('h4').length > 0 ? $h.find('h4') : $h.find('h3');
this.set('composer.reply', this.get('composer.title'));
if (header.length > 0 && header.text().length > 0) {
this.set('composer.title', header.text().trim());
} else {
const filename = (this.get('composer.featuredLink')||"").split("/").pop();
if (filename && filename.length > 0) {
this.set('composer.title', filename.trim());
}
}
}
},
@computed('composer.title')
isAbsoluteUrl() {
return this.get('composer.titleLength') > 0 && /^(https?:)?\/\/[\w\.\-]+/i.test(this.get('composer.title'));
} }
}); });

View File

@ -146,6 +146,11 @@ const Composer = RestModel.extend({
return categoryIds === undefined || !categoryIds.length || categoryIds.indexOf(categoryId) !== -1; return categoryIds === undefined || !categoryIds.length || categoryIds.indexOf(categoryId) !== -1;
}, },
@computed('canEditTopicFeaturedLink')
titlePlaceholder() {
return this.get('canEditTopicFeaturedLink') ? 'composer.title_or_link_placeholder' : 'composer.title_placeholder';
},
// Determine the appropriate title for this action // Determine the appropriate title for this action
actionTitle: function() { actionTitle: function() {
const topic = this.get('topic'); const topic = this.get('topic');

View File

@ -2,7 +2,7 @@
tabindex="2" tabindex="2"
id="reply-title" id="reply-title"
maxLength=siteSettings.max_topic_title_length maxLength=siteSettings.max_topic_title_length
placeholderKey="composer.title_placeholder" placeholderKey=composer.titlePlaceholder
disabled=composer.loading}} disabled=composer.loading}}
{{popup-input-tip validation=validation}} {{popup-input-tip validation=validation}}

View File

@ -80,11 +80,6 @@
{{/if}} {{/if}}
{{render "additional-composer-buttons" model}} {{render "additional-composer-buttons" model}}
{{/if}} {{/if}}
{{#if model.canEditTopicFeaturedLink}}
<div class="topic-featured-link-input">
{{text-field tabindex="4" type="url" value=model.featuredLink id='topic-featured-link' placeholderKey="composer.topic_featured_link_placeholder"}}
</div>
{{/if}}
</div> </div>
{{/if}} {{/if}}
{{plugin-outlet "composer-fields"}} {{plugin-outlet "composer-fields"}}

View File

@ -25,9 +25,6 @@
{{category-chooser valueAttribute="id" value=buffered.category_id}} {{category-chooser valueAttribute="id" value=buffered.category_id}}
{{/if}} {{/if}}
{{#if canEditTopicFeaturedLink}}
{{text-field type="url" value=buffered.featured_link id='topic-featured-link' placeholderKey="composer.topic_featured_link_placeholder"}}
{{/if}}
{{#if canEditTags}} {{#if canEditTags}}
<br> <br>
{{tag-chooser tags=buffered.tags categoryId=buffered.category_id}} {{tag-chooser tags=buffered.tags categoryId=buffered.category_id}}

View File

@ -1072,6 +1072,7 @@ en:
users_placeholder: "Add a user" users_placeholder: "Add a user"
title_placeholder: "What is this discussion about in one brief sentence?" title_placeholder: "What is this discussion about in one brief sentence?"
title_or_link_placeholder: "Type title, or paste a link here"
edit_reason_placeholder: "why are you editing?" edit_reason_placeholder: "why are you editing?"
show_edit_reason: "(add edit reason)" show_edit_reason: "(add edit reason)"
topic_featured_link_placeholder: "Enter link shown with title." topic_featured_link_placeholder: "Enter link shown with title."

View File

@ -0,0 +1,42 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Composer topic featured links", {
loggedIn: true,
settings: {
topic_featured_link_enabled: true
}
});
test("onebox with title", () => {
visit("/");
click('#create-topic');
fillIn('#reply-title', "http://www.example.com/has-title.html");
andThen(() => {
ok(find('.d-editor-preview').html().trim().indexOf('onebox') > 0, "it pastes the link into the body and previews it");
ok(exists('.d-editor-textarea-wrapper .popup-tip.good'), 'the body is now good');
equal(find('.title-input input').val(), "An interesting article", "title is from the oneboxed article");
});
});
test("onebox result doesn't include a title", () => {
visit("/");
click('#create-topic');
fillIn('#reply-title', 'http://www.example.com/no-title.html');
andThen(() => {
ok(find('.d-editor-preview').html().trim().indexOf('onebox') > 0, "it pastes the link into the body and previews it");
ok(exists('.d-editor-textarea-wrapper .popup-tip.good'), 'the body is now good');
equal(find('.title-input input').val(), "no-title.html", "title is from the end of the url");
});
});
test("no onebox result", () => {
visit("/");
click('#create-topic');
fillIn('#reply-title', "http://www.example.com/nope-onebox.html");
andThen(() => {
equal(find('.d-editor-preview').html().trim().indexOf('onebox'), -1, "link isn't put into the post");
equal(find('.d-editor-input').val().length, 0, "link isn't put into the post");
equal(find('.title-input input').val(), "http://www.example.com/nope-onebox.html", "title is unchanged");
});
});

View File

@ -320,6 +320,26 @@ export default function() {
this.delete('/admin/users/:user_id/revoke_api_key', success); this.delete('/admin/users/:user_id/revoke_api_key', success);
this.post('/admin/badges', success); this.post('/admin/badges', success);
this.delete('/admin/badges/:id', success); this.delete('/admin/badges/:id', success);
this.get('/onebox', request => {
if (request.queryParams.url === 'http://www.example.com/has-title.html') {
return [
200,
{"Content-Type": "application/html"},
'<aside class="onebox"><article class="onebox-body"><h3><a href="http://www.example.com/article.html">An interesting article</a></h3></article></aside>'
];
}
if (request.queryParams.url === 'http://www.example.com/no-title.html') {
return [
200,
{"Content-Type": "application/html"},
'<aside class="onebox"><article class="onebox-body"><p>No title</p></article></aside>'
];
}
return [404, {"Content-Type": "application/html"}, ''];;
});
}); });
server.prepareBody = function(body){ server.prepareBody = function(body){

View File

@ -231,3 +231,19 @@ test("Title length for static page topics as admin", function() {
composer.set('title', ''); composer.set('title', '');
ok(!composer.get('titleLengthValid'), "admins must set title to at least 1 character"); ok(!composer.get('titleLengthValid'), "admins must set title to at least 1 character");
}); });
test("title placeholder depends on what you're doing", function() {
let composer = createComposer({action: Composer.CREATE_TOPIC});
equal(composer.get('titlePlaceholder'), 'composer.title_placeholder', "placeholder for normal topic");
composer = createComposer({action: Composer.PRIVATE_MESSAGE});
equal(composer.get('titlePlaceholder'), 'composer.title_placeholder', "placeholder for private message");
Discourse.SiteSettings.topic_featured_link_enabled = true;
composer = createComposer({action: Composer.CREATE_TOPIC});
equal(composer.get('titlePlaceholder'), 'composer.title_or_link_placeholder', "placeholder invites you to paste a link");
composer = createComposer({action: Composer.PRIVATE_MESSAGE});
equal(composer.get('titlePlaceholder'), 'composer.title_placeholder', "placeholder for private message with topic links enabled");
});