UX: Move likes into drop down instead of its own status line

This commit is contained in:
Robin Ward 2015-07-03 14:57:07 -04:00
parent 5cb8f3bce5
commit db75774440
20 changed files with 306 additions and 238 deletions

View file

@ -0,0 +1,63 @@
import StringBuffer from 'discourse/mixins/string-buffer';
import { iconHTML } from 'discourse/helpers/fa-icon';
export default Ember.Component.extend(StringBuffer, {
tagName: 'section',
classNameBindings: [':post-actions', 'hidden'],
actionsSummary: Em.computed.alias('post.actionsWithoutLikes'),
emptySummary: Em.computed.empty('actionsSummary'),
hidden: Em.computed.and('emptySummary', 'post.notDeleted'),
rerenderTriggers: ['actionsSummary.@each', 'post.deleted'],
// This was creating way too many bound ifs and subviews in the handlebars version.
renderString(buffer) {
if (!this.get('emptySummary')) {
this.get('actionsSummary').forEach(function(c) {
buffer.push("<div class='post-action'>");
const renderActionIf = function(property, dataAttribute, text) {
if (!c.get(property)) { return; }
buffer.push(" <span class='action-link " + dataAttribute +"-action'><a href='#' data-" + dataAttribute + "='" + c.get('id') + "'>" + text + "</a>.</span>");
};
// TODO multi line expansion for flags
buffer.push(c.get('description') + '.');
renderActionIf('can_undo', 'undo', I18n.t("post.actions.undo." + c.get('actionType.name_key')));
renderActionIf('can_defer_flags', 'defer-flags', I18n.t("post.actions.defer_flags", { count: c.count }));
buffer.push("</div>");
});
}
const post = this.get('post');
if (!post.get('deleted')) {
buffer.push("<div class='post-action'>" +
iconHTML('fa-trash-o') + '&nbsp;' +
Discourse.Utilities.tinyAvatar(post.get('postDeletedBy.avatar_template'), {title: post.get('postDeletedBy.username')}) +
Discourse.Formatter.autoUpdatingRelativeAge(new Date(post.get('postDeletedAt'))) +
"</div>");
}
},
actionTypeById(actionTypeId) {
return this.get('actionsSummary').findProperty('id', actionTypeId);
},
click(e) {
const $target = $(e.target);
let actionTypeId;
const post = this.get('post');
if (actionTypeId = $target.data('defer-flags')) {
this.actionTypeById(actionTypeId).deferFlags(post);
return false;
}
if (actionTypeId = $target.data('undo')) {
this.get('actionsSummary').findProperty('id', actionTypeId).undo(post);
return false;
}
return false;
}
});

View file

@ -1,98 +0,0 @@
import StringBuffer from 'discourse/mixins/string-buffer';
export default Em.Component.extend(StringBuffer, {
tagName: 'section',
classNameBindings: [':post-actions', 'hidden'],
actionsHistory: Em.computed.alias('post.actionsHistory'),
emptyHistory: Em.computed.empty('actionsHistory'),
hidden: Em.computed.and('emptyHistory', 'post.notDeleted'),
rerenderTriggers: ['actionsHistory.@each', 'actionsHistory.users.length', 'post.deleted'],
// This was creating way too many bound ifs and subviews in the handlebars version.
renderString(buffer) {
if (!this.get('emptyHistory')) {
this.get('actionsHistory').forEach(function(c) {
buffer.push("<div class='post-action'>");
const renderActionIf = function(property, dataAttribute, text) {
if (!c.get(property)) { return; }
buffer.push(" <span class='action-link " + dataAttribute +"-action'><a href='#' data-" + dataAttribute + "='" + c.get('id') + "'>" + text + "</a>.</span>");
};
// TODO multi line expansion for flags
let iconsHtml = "";
if (c.get('usersExpanded')) {
let postUrl;
c.get('users').forEach(function(u) {
iconsHtml += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username_lower') + "\">";
if (u.post_url) {
postUrl = postUrl || u.post_url;
}
iconsHtml += Discourse.Utilities.avatarImg({
size: 'small',
avatarTemplate: u.get('avatarTemplate'),
title: u.get('username')
});
iconsHtml += "</a>";
});
let key = 'post.actions.people.' + c.get('actionType.name_key');
if (postUrl) { key = key + "_with_url"; }
// TODO postUrl might be uninitialized? pick a good default
buffer.push(" " + I18n.t(key, { icons: iconsHtml, postUrl: postUrl}) + ".");
}
renderActionIf('usersCollapsed', 'who-acted', c.get('description'));
renderActionIf('canAlsoAction', 'act', I18n.t("post.actions.it_too." + c.get('actionType.name_key')));
renderActionIf('can_undo', 'undo', I18n.t("post.actions.undo." + c.get('actionType.name_key')));
renderActionIf('can_defer_flags', 'defer-flags', I18n.t("post.actions.defer_flags", { count: c.count }));
buffer.push("</div>");
});
}
const post = this.get('post');
if (post.get('deleted')) {
buffer.push("<div class='post-action'>" +
"<i class='fa fa-trash-o'></i>&nbsp;" +
Discourse.Utilities.tinyAvatar(post.get('postDeletedBy.avatar_template'), {title: post.get('postDeletedBy.username')}) +
Discourse.Formatter.autoUpdatingRelativeAge(new Date(post.get('postDeletedAt'))) +
"</div>");
}
},
actionTypeById(actionTypeId) {
return this.get('actionsHistory').findProperty('id', actionTypeId);
},
click(e) {
const $target = $(e.target);
let actionTypeId;
const post = this.get('post');
if (actionTypeId = $target.data('defer-flags')) {
this.actionTypeById(actionTypeId).deferFlags(post);
return false;
}
// User wants to know who actioned it
if (actionTypeId = $target.data('who-acted')) {
this.actionTypeById(actionTypeId).loadUsers(post);
return false;
}
if (actionTypeId = $target.data('act')) {
this.get('actionsHistory').findProperty('id', actionTypeId).act(post);
return false;
}
if (actionTypeId = $target.data('undo')) {
this.get('actionsHistory').findProperty('id', actionTypeId).undo(post);
return false;
}
return false;
}
});

View file

@ -1,7 +1,8 @@
import StringBuffer from 'discourse/mixins/string-buffer'; import StringBuffer from 'discourse/mixins/string-buffer';
import { iconHTML } from 'discourse/helpers/fa-icon';
// Helper class for rendering a button // Helper class for rendering a button
export var Button = function(action, label, icon, opts) { export const Button = function(action, label, icon, opts) {
this.action = action; this.action = action;
this.label = label; this.label = label;
@ -18,7 +19,7 @@ function animateHeart($elem, start, end, complete) {
.css('textIndent', start) .css('textIndent', start)
.animate({ textIndent: end }, { .animate({ textIndent: end }, {
complete: complete, complete: complete,
step: function(now) { step(now) {
$(this).css('transform','scale('+now+')'); $(this).css('transform','scale('+now+')');
}, },
duration: 150 duration: 150
@ -26,9 +27,9 @@ function animateHeart($elem, start, end, complete) {
} }
Button.prototype.render = function(buffer) { Button.prototype.render = function(buffer) {
var opts = this.opts; const opts = this.opts;
var label = I18n.t(this.label); const label = I18n.t(this.label);
buffer.push("<button aria-label=\"" + label +"\" " + "title=\"" + label + "\""); buffer.push("<button aria-label=\"" + label +"\" " + "title=\"" + label + "\"");
if (opts.disabled) { buffer.push(" disabled"); } if (opts.disabled) { buffer.push(" disabled"); }
@ -36,21 +37,22 @@ Button.prototype.render = function(buffer) {
if (opts.shareUrl) { buffer.push(" data-share-url=\"" + opts.shareUrl + "\""); } if (opts.shareUrl) { buffer.push(" data-share-url=\"" + opts.shareUrl + "\""); }
if (opts.postNumber) { buffer.push(" data-post-number=\"" + opts.postNumber + "\""); } if (opts.postNumber) { buffer.push(" data-post-number=\"" + opts.postNumber + "\""); }
buffer.push(" data-action=\"" + this.action + "\">"); buffer.push(" data-action=\"" + this.action + "\">");
if (this.icon) { buffer.push("<i class=\"fa fa-" + this.icon + "\"></i>"); } if (this.icon) { buffer.push(iconHTML(this.icon)); }
if (opts.textLabel) { buffer.push(I18n.t(opts.textLabel)); } if (opts.textLabel) { buffer.push(I18n.t(opts.textLabel)); }
if (opts.innerHTML) { buffer.push(opts.innerHTML); } if (opts.innerHTML) { buffer.push(opts.innerHTML); }
buffer.push("</button>"); buffer.push("</button>");
}; };
var hiddenButtons; let hiddenButtons;
var PostMenuView = Discourse.View.extend(StringBuffer, { const PostMenuView = Ember.Component.extend(StringBuffer, {
tagName: 'section', tagName: 'section',
classNames: ['post-menu-area', 'clearfix'], classNames: ['post-menu-area', 'clearfix'],
rerenderTriggers: [ rerenderTriggers: [
'post.deleted_at', 'post.deleted_at',
'post.like_count', 'likeAction.count',
'likeAction.users.length',
'post.reply_count', 'post.reply_count',
'post.showRepliesBelow', 'post.showRepliesBelow',
'post.can_delete', 'post.can_delete',
@ -62,53 +64,71 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
'post.post_type', 'post.post_type',
'collapsed'], 'collapsed'],
likeAction: function() {
return this.get('post.actionByName.like');
}.property('post.actionByName.like'),
_collapsedByDefault: function() { _collapsedByDefault: function() {
this.set('collapsed', true); this.set('collapsed', true);
}.on('init'), }.on('init'),
renderString: function(buffer) { renderString(buffer) {
var post = this.get('post'); const post = this.get('post');
buffer.push("<nav class='post-controls'>"); buffer.push("<nav class='post-controls'>");
this.renderReplies(post, buffer); this.renderReplies(post, buffer);
this.renderLikes(post, buffer);
this.renderButtons(post, buffer); this.renderButtons(post, buffer);
this.renderAdminPopup(post, buffer); this.renderAdminPopup(post, buffer);
buffer.push("</nav>"); buffer.push("</nav>");
}, },
// Delegate click actions // Delegate click actions
click: function(e) { click(e) {
var $target = $(e.target), const $target = $(e.target),
action = $target.data('action') || $target.parent().data('action'); action = $target.data('action') || $target.parent().data('action');
if (!action) return; if (!action) return;
var handler = this["click" + action.capitalize()]; const handler = this["click" + action.capitalize()];
if (!handler) return; if (!handler) return;
handler.call(this, this.get('post')); handler.call(this, this.get('post'));
}, },
// Replies Button // Replies Button
renderReplies: function(post, buffer) { renderReplies(post, buffer) {
if (!post.get('showRepliesBelow')) return; if (!post.get('showRepliesBelow')) return;
var reply_count = post.get('reply_count'); const replyCount = post.get('reply_count');
buffer.push("<button class='show-replies' data-action='replies'>"); buffer.push("<button class='show-replies' data-action='replies'>");
buffer.push("<span class='badge-posts'>" + reply_count + "</span>"); buffer.push("<span class='badge-posts'>" + replyCount + "</span>");
buffer.push(I18n.t("post.has_replies", { count: reply_count })); buffer.push(I18n.t("post.has_replies", { count: replyCount }));
var icon = (this.get('post.replies.length') > 0) ? 'fa-chevron-up' : 'fa-chevron-down'; const icon = (this.get('post.replies.length') > 0) ? 'chevron-up' : 'chevron-down';
return buffer.push("<i class='fa " + icon + "'></i></button>"); return buffer.push(iconHTML(icon) + "</button>");
}, },
renderButtons: function(post, buffer) { renderLikes(post, buffer) {
var self = this, const likeCount = this.get('likeAction.count') || 0;
allButtons = [], if (likeCount === 0) { return; }
visibleButtons = [];
buffer.push("<button class='show-likes' data-action='likes'>");
buffer.push("<span class='badge-posts'>" + likeCount + "</span>");
buffer.push(I18n.t("post.has_likes", { count: likeCount }));
const icon = (this.get('likeAction.users.length') > 0) ? 'chevron-up' : 'chevron-down';
return buffer.push(iconHTML(icon) + "</button>");
},
renderButtons(post, buffer) {
const self = this;
const allButtons = [];
let visibleButtons = [];
if (typeof hiddenButtons === "undefined") { if (typeof hiddenButtons === "undefined") {
if (!Em.isEmpty(Discourse.SiteSettings.post_menu_hidden_items)) { if (!Em.isEmpty(this.siteSettings.post_menu_hidden_items)) {
hiddenButtons = Discourse.SiteSettings.post_menu_hidden_items.split('|'); hiddenButtons = this.siteSettings.post_menu_hidden_items.split('|');
} else { } else {
hiddenButtons = []; hiddenButtons = [];
} }
@ -118,11 +138,11 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
hiddenButtons.removeObject("bookmark"); hiddenButtons.removeObject("bookmark");
} }
var yours = post.get('yours'); const yours = post.get('yours');
Discourse.SiteSettings.post_menu.split("|").forEach(function(i) { this.siteSettings.post_menu.split("|").forEach(function(i) {
var creator = self["buttonFor" + i.replace(/\+/, '').capitalize()]; const creator = self["buttonFor" + i.replace(/\+/, '').capitalize()];
if (creator) { if (creator) {
var button = creator.call(self, post); const button = creator.call(self, post);
if (button) { if (button) {
allButtons.push(button); allButtons.push(button);
if ((yours && button.opts.alwaysShowYours) || if ((yours && button.opts.alwaysShowYours) ||
@ -136,7 +156,7 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
// Only show ellipsis if there is more than one button hidden // Only show ellipsis if there is more than one button hidden
// if there are no more buttons, we are not collapsed // if there are no more buttons, we are not collapsed
var collapsed = this.get('collapsed'); const collapsed = this.get('collapsed');
if (!collapsed || (allButtons.length <= visibleButtons.length + 1)) { if (!collapsed || (allButtons.length <= visibleButtons.length + 1)) {
visibleButtons = allButtons; visibleButtons = allButtons;
if (collapsed) { this.set('collapsed', false); } if (collapsed) { this.set('collapsed', false); }
@ -144,7 +164,7 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
visibleButtons.splice(visibleButtons.length - 1, 0, this.buttonForShowMoreActions(post)); visibleButtons.splice(visibleButtons.length - 1, 0, this.buttonForShowMoreActions(post));
} }
var callbacks = PostMenuView._registerButtonCallbacks; const callbacks = PostMenuView._registerButtonCallbacks;
if (callbacks) { if (callbacks) {
_.each(callbacks, function(callback) { _.each(callbacks, function(callback) {
callback.apply(self, [visibleButtons]); callback.apply(self, [visibleButtons]);
@ -152,13 +172,23 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
} }
buffer.push('<div class="actions">'); buffer.push('<div class="actions">');
visibleButtons.forEach(function (b) { visibleButtons.forEach((b) => b.render(buffer));
b.render(buffer);
});
buffer.push("</div>"); buffer.push("</div>");
}, },
clickReplies: function() { clickLikes() {
const likeAction = this.get('post.actionByName.like');
if (likeAction) {
const users = likeAction.get('users');
if (users && users.length) {
users.clear();
} else {
likeAction.loadUsers(this.get('post'));
}
}
},
clickReplies() {
if (this.get('post.replies.length') > 0) { if (this.get('post.replies.length') > 0) {
this.set('post.replies', []); this.set('post.replies', []);
} else { } else {
@ -167,12 +197,12 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
}, },
// Delete button // Delete button
buttonForDelete: function(post) { buttonForDelete(post) {
var label, icon; let label, icon;
if (post.get('post_number') === 1) { if (post.get('post_number') === 1) {
// If it's the first post, the delete/undo actions are related to the topic // If it's the first post, the delete/undo actions are related to the topic
var topic = post.get('topic'); const topic = post.get('topic');
if (topic.get('deleted_at')) { if (topic.get('deleted_at')) {
if (!topic.get('details.can_recover')) { return; } if (!topic.get('details.can_recover')) { return; }
label = "topic.actions.recover"; label = "topic.actions.recover";
@ -195,50 +225,50 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
icon = "trash-o"; icon = "trash-o";
} }
} }
var action = (icon === 'trash-o') ? 'delete' : 'recover'; const action = (icon === 'trash-o') ? 'delete' : 'recover';
var opts; let opts;
if (icon === "trash-o"){ if (icon === "trash-o"){
opts = {className: 'delete'}; opts = {className: 'delete'};
} }
return new Button(action, label, icon, opts); return new Button(action, label, icon, opts);
}, },
clickRecover: function(post) { clickRecover(post) {
this.get('controller').send('recoverPost', post); this.sendAction('recoverPost', post);
}, },
clickDelete: function(post) { clickDelete(post) {
this.get('controller').send('deletePost', post); this.sendAction('deletePost', post);
}, },
// Like button // Like button
buttonForLike: function(post) { buttonForLike(post) {
var likeAction = post.get('actionByName.like'); const likeAction = this.get('likeAction');
if (!likeAction) { return; } if (!likeAction) { return; }
var className = likeAction.get('acted') ? 'has-like' : 'like'; const className = likeAction.get('acted') ? 'has-like' : 'like';
if (likeAction.get('canToggle')) { if (likeAction.get('canToggle')) {
var descKey = likeAction.get('acted') ? 'post.controls.undo_like' : 'post.controls.like'; const descKey = likeAction.get('acted') ? 'post.controls.undo_like' : 'post.controls.like';
return new Button('like', descKey, 'heart', {className: className}); return new Button('like', descKey, 'heart', {className: className});
} else if (likeAction.get('acted')) { } else if (likeAction.get('acted')) {
return new Button('like', 'post.controls.has_liked', 'heart', {className: className, disabled: true}); return new Button('like', 'post.controls.has_liked', 'heart', {className: className, disabled: true});
} }
}, },
clickLike: function(post) { clickLike(post) {
var $heart = this.$('.fa-heart'), const $heart = this.$('.fa-heart'),
controller = this.get('controller'), $likeButton = this.$('button[data-action=like]'),
$likeButton = this.$('button[data-action=like]'); acted = post.get('actionByName.like.acted'),
self = this;
var acted = post.get('actionByName.like.acted');
if (acted) { if (acted) {
controller.send('toggleLike', post); this.sendAction('toggleLike', post);
$likeButton.removeClass('has-like').addClass('like'); $likeButton.removeClass('has-like').addClass('like');
} else { } else {
var scale = [1.0, 1.5]; const scale = [1.0, 1.5];
animateHeart($heart, scale[0], scale[1], function() { animateHeart($heart, scale[0], scale[1], function() {
animateHeart($heart, scale[1], scale[0], function() { animateHeart($heart, scale[1], scale[0], function() {
controller.send('toggleLike', post); self.sendAction('toggleLike', post);
$likeButton.removeClass('like').addClass('has-like'); $likeButton.removeClass('like').addClass('has-like');
}); });
}); });
@ -246,17 +276,17 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
}, },
// Flag button // Flag button
buttonForFlag: function(post) { buttonForFlag(post) {
if (Em.isEmpty(post.get('flagsAvailable'))) return; if (Em.isEmpty(post.get('flagsAvailable'))) return;
return new Button('flag', 'post.controls.flag', 'flag'); return new Button('flag', 'post.controls.flag', 'flag');
}, },
clickFlag: function(post) { clickFlag(post) {
this.get('controller').send('showFlags', post); this.sendAction('showFlags', post);
}, },
// Edit button // Edit button
buttonForEdit: function(post) { buttonForEdit(post) {
if (!post.get('can_edit')) return; if (!post.get('can_edit')) return;
return new Button('edit', 'post.controls.edit', 'pencil', { return new Button('edit', 'post.controls.edit', 'pencil', {
alwaysShowYours: true, alwaysShowYours: true,
@ -264,14 +294,14 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
}); });
}, },
clickEdit: function(post) { clickEdit(post) {
this.get('controller').send('editPost', post); this.sendAction('editPost', post);
}, },
// Share button // Share button
buttonForShare: function(post) { buttonForShare(post) {
if (!Discourse.User.current()) return; if (!Discourse.User.current()) return;
var options = { const options = {
shareUrl: post.get('shareUrl'), shareUrl: post.get('shareUrl'),
postNumber: post.get('post_number') postNumber: post.get('post_number')
}; };
@ -279,9 +309,9 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
}, },
// Reply button // Reply button
buttonForReply: function() { buttonForReply() {
if (!this.get('controller.model.details.can_create_post')) return; if (!this.get('canCreatePost')) return;
var options = {className: 'create'}; const options = {className: 'create'};
if(!Discourse.Mobile.mobileView) { if(!Discourse.Mobile.mobileView) {
options.textLabel = 'topic.reply.title'; options.textLabel = 'topic.reply.title';
@ -290,15 +320,15 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
return new Button('reply', 'post.controls.reply', 'reply', options); return new Button('reply', 'post.controls.reply', 'reply', options);
}, },
clickReply: function(post) { clickReply(post) {
this.get('controller').send('replyToPost', post); this.sendAction('replyToPost', post);
}, },
// Bookmark button // Bookmark button
buttonForBookmark: function(post) { buttonForBookmark(post) {
if (!Discourse.User.current()) return; if (!Discourse.User.current()) return;
var iconClass = 'read-icon', let iconClass = 'read-icon',
buttonClass = 'bookmark', buttonClass = 'bookmark',
tooltip = 'bookmarks.not_bookmarked'; tooltip = 'bookmarks.not_bookmarked';
@ -311,33 +341,30 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
return new Button('bookmark', tooltip, {className: buttonClass, innerHTML: "<div class='" + iconClass + "'>"}); return new Button('bookmark', tooltip, {className: buttonClass, innerHTML: "<div class='" + iconClass + "'>"});
}, },
clickBookmark: function(post) { clickBookmark(post) {
this.get('controller').send('toggleBookmark', post); this.sendAction('toggleBookmark', post);
}, },
buttonForAdmin: function() { buttonForAdmin() {
if (!Discourse.User.currentProp('canManageTopic')) { return; } if (!Discourse.User.currentProp('canManageTopic')) { return; }
return new Button('admin', 'post.controls.admin', 'wrench'); return new Button('admin', 'post.controls.admin', 'wrench');
}, },
renderAdminPopup: function(post, buffer) { renderAdminPopup(post, buffer) {
if (!Discourse.User.currentProp('canManageTopic')) { return; } if (!Discourse.User.currentProp('canManageTopic')) { return; }
var isWiki = post.get('wiki'), const isWiki = post.get('wiki'),
wikiIcon = '<i class="fa fa-pencil-square-o"></i>', wikiIcon = iconHTML('pencil-square-o'),
wikiText = isWiki ? I18n.t('post.controls.unwiki') : I18n.t('post.controls.wiki'); wikiText = isWiki ? I18n.t('post.controls.unwiki') : I18n.t('post.controls.wiki'),
isModerator = post.get('post_type') === this.site.get('post_types.moderator_action'),
postTypeIcon = iconHTML('shield'),
postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator'),
rebakePostIcon = iconHTML('cog'),
rebakePostText = I18n.t('post.controls.rebake'),
unhidePostIcon = iconHTML('eye'),
unhidePostText = I18n.t('post.controls.unhide');
var isModerator = post.get('post_type') === Discourse.Site.currentProp('post_types.moderator_action'), const html = '<div class="post-admin-menu">' +
postTypeIcon = '<i class="fa fa-shield"></i>',
postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator');
var rebakePostIcon = '<i class="fa fa-cog"></i>',
rebakePostText = I18n.t('post.controls.rebake');
var unhidePostIcon = '<i class="fa fa-eye"></i>',
unhidePostText = I18n.t('post.controls.unhide');
var html = '<div class="post-admin-menu">' +
'<h3>' + I18n.t('admin_title') + '</h3>' + '<h3>' + I18n.t('admin_title') + '</h3>' +
'<ul>' + '<ul>' +
'<li class="btn btn-admin" data-action="toggleWiki">' + wikiIcon + wikiText + '</li>' + '<li class="btn btn-admin" data-action="toggleWiki">' + wikiIcon + wikiText + '</li>' +
@ -350,8 +377,8 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
buffer.push(html); buffer.push(html);
}, },
clickAdmin: function() { clickAdmin() {
var $postAdminMenu = this.$(".post-admin-menu"); const $postAdminMenu = this.$(".post-admin-menu");
$postAdminMenu.show(); $postAdminMenu.show();
$("html").on("mouseup.post-admin-menu", function() { $("html").on("mouseup.post-admin-menu", function() {
$postAdminMenu.hide(); $postAdminMenu.hide();
@ -359,34 +386,34 @@ var PostMenuView = Discourse.View.extend(StringBuffer, {
}); });
}, },
clickToggleWiki: function() { clickToggleWiki() {
this.get('controller').send('toggleWiki', this.get('post')); this.sendAction('toggleWiki', this.get('post'));
}, },
clickTogglePostType: function () { clickTogglePostType() {
this.get("controller").send("togglePostType", this.get("post")); this.sendAction("togglePostType", this.get("post"));
}, },
clickRebakePost: function () { clickRebakePost() {
this.get("controller").send("rebakePost", this.get("post")); this.sendAction("rebakePost", this.get("post"));
}, },
clickUnhidePost: function () { clickUnhidePost() {
this.get("controller").send("unhidePost", this.get("post")); this.sendAction("unhidePost", this.get("post"));
}, },
buttonForShowMoreActions: function() { buttonForShowMoreActions() {
return new Button('showMoreActions', 'show_more', 'ellipsis-h'); return new Button('showMoreActions', 'show_more', 'ellipsis-h');
}, },
clickShowMoreActions: function() { clickShowMoreActions() {
this.set('collapsed', false); this.set('collapsed', false);
} }
}); });
PostMenuView.reopenClass({ PostMenuView.reopenClass({
registerButton: function(callback){ registerButton(callback){
this._registerButtonCallbacks = this._registerButtonCallbacks || []; this._registerButtonCallbacks = this._registerButtonCallbacks || [];
this._registerButtonCallbacks.push(callback); this._registerButtonCallbacks.push(callback);
} }

View file

@ -0,0 +1,24 @@
import StringBuffer from 'discourse/mixins/string-buffer';
export default Ember.Component.extend(StringBuffer, {
classNames: ['who-liked'],
likedUsers: Ember.computed.alias('post.actionByName.like.users'),
rerenderTriggers: ['likedUsers.length'],
renderString(buffer) {
const likedUsers = this.get('likedUsers');
if (likedUsers) {
let iconsHtml = "";
likedUsers.forEach(function(u) {
iconsHtml += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username_lower') + "\">";
iconsHtml += Discourse.Utilities.avatarImg({
size: 'small',
avatarTemplate: u.get('avatarTemplate'),
title: u.get('username')
});
iconsHtml += "</a>";
});
buffer.push(iconsHtml);
}
}
});

View file

@ -1,4 +1,5 @@
import ObjectController from 'discourse/controllers/object'; import ObjectController from 'discourse/controllers/object';
import { MAX_MESSAGE_LENGTH } from 'discourse/models/post-action-type';
// Supports logic for flags in the modal // Supports logic for flags in the modal
export default ObjectController.extend({ export default ObjectController.extend({
@ -38,7 +39,7 @@ export default ObjectController.extend({
return I18n.t("flagging.custom_message.more", { n: minLen - len }); return I18n.t("flagging.custom_message.more", { n: minLen - len });
} else { } else {
return I18n.t("flagging.custom_message.left", { return I18n.t("flagging.custom_message.left", {
n: Discourse.PostActionType.MAX_MESSAGE_LENGTH - len n: MAX_MESSAGE_LENGTH - len
}); });
} }
}.property('message.length') }.property('message.length')

View file

@ -1,5 +1,6 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality'; import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object'; import ObjectController from 'discourse/controllers/object';
import { MAX_MESSAGE_LENGTH } from 'discourse/models/post-action-type';
export default ObjectController.extend(ModalFunctionality, { export default ObjectController.extend(ModalFunctionality, {
userDetails: null, userDetails: null,
@ -42,7 +43,7 @@ export default ObjectController.extend(ModalFunctionality, {
if (selected.get('is_custom_flag')) { if (selected.get('is_custom_flag')) {
const len = this.get('message.length') || 0; const len = this.get('message.length') || 0;
return len >= Discourse.SiteSettings.min_private_message_post_length && return len >= Discourse.SiteSettings.min_private_message_post_length &&
len <= Discourse.PostActionType.MAX_MESSAGE_LENGTH; len <= MAX_MESSAGE_LENGTH;
} }
return true; return true;
}.property('selected.is_custom_flag', 'message.length'), }.property('selected.is_custom_flag', 'message.length'),

View file

@ -24,6 +24,9 @@ export default {
injectAll(app, 'appEvents'); injectAll(app, 'appEvents');
Discourse.URL.appEvents = appEvents; Discourse.URL.appEvents = appEvents;
app.register('store:main', Store);
inject(app, 'store', 'route', 'controller');
// Inject Discourse.Site to avoid using Discourse.Site.current() // Inject Discourse.Site to avoid using Discourse.Site.current()
const site = Discourse.Site.current(); const site = Discourse.Site.current();
app.register('site:main', site, { instantiate: false }); app.register('site:main', site, { instantiate: false });
@ -37,9 +40,6 @@ export default {
app.register('session:main', Session.current(), { instantiate: false }); app.register('session:main', Session.current(), { instantiate: false });
injectAll(app, 'session'); injectAll(app, 'session');
app.register('store:main', Store);
inject(app, 'store', 'route', 'controller');
app.register('current-user:main', Discourse.User.current(), { instantiate: false }); app.register('current-user:main', Discourse.User.current(), { instantiate: false });
inject(app, 'currentUser', 'component', 'route', 'controller'); inject(app, 'currentUser', 'component', 'route', 'controller');

View file

@ -5,7 +5,7 @@ export default RestModel.extend({
// Description for the action // Description for the action
description: function() { description: function() {
var action = this.get('actionType.name_key'); const action = this.get('actionType.name_key');
if (this.get('acted')) { if (this.get('acted')) {
if (this.get('count') <= 1) { if (this.get('count') <= 1) {
return I18n.t('post.actions.by_you.' + action); return I18n.t('post.actions.by_you.' + action);
@ -17,7 +17,6 @@ export default RestModel.extend({
} }
}.property('count', 'acted', 'actionType'), }.property('count', 'acted', 'actionType'),
canAlsoAction: Em.computed.and('can_act', 'actionType.notCustomFlag'),
usersCollapsed: Em.computed.not('usersExpanded'), usersCollapsed: Em.computed.not('usersExpanded'),
usersExpanded: Em.computed.gt('users.length', 0), usersExpanded: Em.computed.gt('users.length', 0),
@ -51,7 +50,7 @@ export default RestModel.extend({
act: function(post, opts) { act: function(post, opts) {
if (!opts) opts = {}; if (!opts) opts = {};
var action = this.get('actionType.name_key'); const action = this.get('actionType.name_key');
// Mark it as acted // Mark it as acted
this.setProperties({ this.setProperties({
@ -72,7 +71,7 @@ export default RestModel.extend({
} }
// Create our post action // Create our post action
var self = this; const self = this;
return Discourse.ajax("/post_actions", { return Discourse.ajax("/post_actions", {
type: 'POST', type: 'POST',
@ -109,7 +108,7 @@ export default RestModel.extend({
}, },
deferFlags: function(post) { deferFlags: function(post) {
var self = this; const self = this;
return Discourse.ajax("/post_actions/defer_flags", { return Discourse.ajax("/post_actions/defer_flags", {
type: "POST", type: "POST",
data: { data: {
@ -122,16 +121,16 @@ export default RestModel.extend({
}, },
loadUsers: function(post) { loadUsers: function(post) {
var self = this; const self = this;
Discourse.ajax("/post_actions/users", { Discourse.ajax("/post_actions/users", {
data: { data: {
id: post.get('id'), id: post.get('id'),
post_action_type_id: this.get('id') post_action_type_id: this.get('id')
} }
}).then(function (result) { }).then(function (result) {
var users = Em.A(); const users = [];
self.set('users', users); self.set('users', users);
_.each(result,function(user) { result.forEach(function(user) {
if (user.id === Discourse.User.currentProp('id')) { if (user.id === Discourse.User.currentProp('id')) {
users.pushObject(Discourse.User.current()); users.pushObject(Discourse.User.current());
} else { } else {

View file

@ -0,0 +1,9 @@
import RestModel from 'discourse/models/rest';
const PostActionType = RestModel.extend({
notCustomFlag: Em.computed.not('is_custom_flag')
});
export const MAX_MESSAGE_LENGTH = 500;
export default PostActionType;

View file

@ -108,11 +108,12 @@ const Post = RestModel.extend({
}); });
}.property('actions_summary.@each.can_act'), }.property('actions_summary.@each.can_act'),
actionsHistory: function() { actionsWithoutLikes: function() {
if (!this.present('actions_summary')) return null; if (!this.present('actions_summary')) return null;
return this.get('actions_summary').filter(function(i) { return this.get('actions_summary').filter(function(i) {
if (i.get('count') === 0) return false; if (i.get('count') === 0) return false;
if (i.get('actionType.name_key') === 'like') { return false; }
if (i.get('users') && i.get('users').length > 0) return true; if (i.get('users') && i.get('users').length > 0) return true;
return !i.get('hidden'); return !i.get('hidden');
}); });

View file

@ -1,15 +0,0 @@
/**
A data model representing action types (flags, likes) against a Post
@class PostActionType
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.PostActionType = Discourse.Model.extend({
notCustomFlag: Em.computed.not('is_custom_flag')
});
Discourse.PostActionType.reopenClass({
MAX_MESSAGE_LENGTH: 500
});

View file

@ -1,3 +1,5 @@
import PostActionType from 'discourse/models/post-action-type';
const Site = Discourse.Model.extend({ const Site = Discourse.Model.extend({
isReadOnly: Em.computed.alias('is_readonly'), isReadOnly: Em.computed.alias('is_readonly'),
@ -102,7 +104,7 @@ Site.reopenClass(Discourse.Singleton, {
if (result.post_action_types) { if (result.post_action_types) {
result.postActionByIdLookup = Em.Object.create(); result.postActionByIdLookup = Em.Object.create();
result.post_action_types = _.map(result.post_action_types,function(p) { result.post_action_types = _.map(result.post_action_types,function(p) {
const actionType = Discourse.PostActionType.create(p); const actionType = PostActionType.create(p);
result.postActionByIdLookup.set("action" + p.id, actionType); result.postActionByIdLookup.set("action" + p.id, actionType);
return actionType; return actionType;
}); });
@ -111,7 +113,7 @@ Site.reopenClass(Discourse.Singleton, {
if (result.topic_flag_types) { if (result.topic_flag_types) {
result.topicFlagByIdLookup = Em.Object.create(); result.topicFlagByIdLookup = Em.Object.create();
result.topic_flag_types = _.map(result.topic_flag_types,function(p) { result.topic_flag_types = _.map(result.topic_flag_types,function(p) {
const actionType = Discourse.PostActionType.create(p); const actionType = PostActionType.create(p);
result.topicFlagByIdLookup.set("action" + p.id, actionType); result.topicFlagByIdLookup.set("action" + p.id, actionType);
return actionType; return actionType;
}); });

View file

@ -90,7 +90,20 @@
<button {{action "expandFirstPost" this}} class='btn expand-post'>{{i18n 'post.show_full'}}&hellip;</button> <button {{action "expandFirstPost" this}} class='btn expand-post'>{{i18n 'post.show_full'}}&hellip;</button>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{view 'post-menu' post=this adminMenu=view.adminMenu}}
{{post-menu post=this
canCreatePost=controller.model.details.can_create_post
replyToPost="replyToPost"
recoverPost="recoverPost"
deletePost="deletePost"
toggleLike="toggleLike"
showFlags="showFlags"
editPost="editPost"
toggleBookmark="toggleBookmark"
toggleWiki="toggleWiki"
togglePostType="togglePostType"
rebakePost="rebakePost"
unhidePost="unhidePost"}}
</div> </div>
{{#if replies}} {{#if replies}}
@ -101,7 +114,8 @@
</section> </section>
{{/if}} {{/if}}
{{discourse-action-history post=this}} {{actions-summary post=this}}
{{who-liked post=this}}
{{view 'topic-map-container' post=this topic=controller.model}} {{view 'topic-map-container' post=this topic=controller.model}}
</div> </div>

View file

@ -27,6 +27,7 @@
//= require_tree ./discourse/adapters //= require_tree ./discourse/adapters
//= require ./discourse/models/rest //= require ./discourse/models/rest
//= require ./discourse/models/model //= require ./discourse/models/model
//= require ./discourse/models/post-action-type
//= require ./discourse/models/post //= require ./discourse/models/post
//= require ./discourse/models/post-stream //= require ./discourse/models/post-stream
//= require ./discourse/models/topic-details //= require ./discourse/models/topic-details

View file

@ -233,3 +233,11 @@ blockquote > *:last-child {
} }
} }
.who-liked {
a {
margin: 0 0.25em 0.5em 0;
display: inline-block;
}
}

View file

@ -95,7 +95,7 @@ nav.post-controls {
} }
} }
.show-replies { .show-replies, .show-likes {
margin-left: 0; margin-left: 0;
font-size: inherit; font-size: inherit;
span.badge-posts {color: scale-color($primary, $lightness: 60%);} span.badge-posts {color: scale-color($primary, $lightness: 60%);}

View file

@ -27,6 +27,21 @@ span.badge-posts {
display: none; display: none;
} }
.show-likes {
margin-left: 0;
padding-left: 0;
padding-right: 0;
font-size: inherit;
span.badge-posts {color: scale-color($primary, $lightness: 60%);}
&:hover {
background: dark-light-diff($primary, $secondary, 90%, -65%);
span.badge-posts {color: $primary;}
}
i {
display: none;
}
}
nav.post-controls { nav.post-controls {
clear: both; clear: both;
} }

View file

@ -1233,6 +1233,9 @@ en:
has_replies: has_replies:
one: "Reply" one: "Reply"
other: "Replies" other: "Replies"
has_likes:
one: "Like"
other: "Likes"
errors: errors:
create: "Sorry, there was an error creating your post. Please try again." create: "Sorry, there was an error creating your post. Please try again."

View file

@ -1,3 +1,5 @@
import createStore from 'helpers/create-store';
var buildPost = function(args) { var buildPost = function(args) {
return Discourse.Post.create(_.merge({ return Discourse.Post.create(_.merge({
id: 1, id: 1,
@ -18,14 +20,21 @@ moduleFor("controller:flag", "controller:flag", {
}); });
test("canDeleteSpammer not staff", function(){ test("canDeleteSpammer not staff", function(){
const store = createStore();
var flagController = this.subject({ model: buildPost() }); var flagController = this.subject({ model: buildPost() });
sandbox.stub(Discourse.User, 'currentProp').withArgs('staff').returns(false); sandbox.stub(Discourse.User, 'currentProp').withArgs('staff').returns(false);
flagController.set('selected', Discourse.PostActionType.create({name_key: 'spam'}));
const spamFlag = store.createRecord('post-action-type', {name_key: 'spam'});
flagController.set('selected', spamFlag);
equal(flagController.get('canDeleteSpammer'), false, 'false if current user is not staff'); equal(flagController.get('canDeleteSpammer'), false, 'false if current user is not staff');
}); });
var canDeleteSpammer = function(flagController, postActionType, expected, testName) { var canDeleteSpammer = function(flagController, postActionType, expected, testName) {
flagController.set('selected', Discourse.PostActionType.create({name_key: postActionType})); const store = createStore();
const flag = store.createRecord('post-action-type', {name_key: postActionType});
flagController.set('selected', flag);
equal(flagController.get('canDeleteSpammer'), expected, testName); equal(flagController.get('canDeleteSpammer'), expected, testName);
}; };

View file

@ -81,15 +81,19 @@ var origDebounce = Ember.run.debounce,
flushMap = require('discourse/models/store', null, null, false).flushMap, flushMap = require('discourse/models/store', null, null, false).flushMap,
server; server;
function dup(obj) {
return jQuery.extend(true, {}, obj);
}
QUnit.testStart(function(ctx) { QUnit.testStart(function(ctx) {
server = createPretendServer(); server = createPretendServer();
// Allow our tests to change site settings and have them reset before the next test // Allow our tests to change site settings and have them reset before the next test
Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal); Discourse.SiteSettings = dup(Discourse.SiteSettingsOriginal);
Discourse.BaseUri = "/"; Discourse.BaseUri = "/";
Discourse.BaseUrl = "localhost"; Discourse.BaseUrl = "localhost";
Discourse.User.resetCurrent(); Discourse.User.resetCurrent();
Discourse.Site.resetCurrent(Discourse.Site.create(fixtures['site.json'].site)); Discourse.Site.resetCurrent(Discourse.Site.create(dup(fixtures['site.json'].site)));
Discourse.URL.redirectedTo = null; Discourse.URL.redirectedTo = null;
Discourse.URL.redirectTo = function(url) { Discourse.URL.redirectTo = function(url) {