mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 22:43:33 +08:00
588 lines
14 KiB
JavaScript
588 lines
14 KiB
JavaScript
import { applyDecorators, createWidget } from "discourse/widgets/widget";
|
|
import { avatarAtts } from "discourse/widgets/actions-summary";
|
|
import { h } from "virtual-dom";
|
|
import showModal from "discourse/lib/show-modal";
|
|
|
|
const LIKE_ACTION = 2;
|
|
|
|
function animateHeart($elem, start, end, complete) {
|
|
if (Ember.testing) {
|
|
return Ember.run(this, complete);
|
|
}
|
|
|
|
$elem
|
|
.stop()
|
|
.css("textIndent", start)
|
|
.animate(
|
|
{ textIndent: end },
|
|
{
|
|
complete,
|
|
step(now) {
|
|
$(this)
|
|
.css("transform", "scale(" + now + ")")
|
|
.addClass("d-liked")
|
|
.removeClass("d-unliked");
|
|
},
|
|
duration: 150
|
|
},
|
|
"linear"
|
|
);
|
|
}
|
|
|
|
const _builders = {};
|
|
const _extraButtons = {};
|
|
|
|
export function addButton(name, builder) {
|
|
_extraButtons[name] = builder;
|
|
}
|
|
|
|
function registerButton(name, builder) {
|
|
_builders[name] = builder;
|
|
}
|
|
|
|
export function buildButton(name, widget) {
|
|
let { attrs, state, siteSettings, settings } = widget;
|
|
let builder = _builders[name];
|
|
if (builder) {
|
|
let button = builder(attrs, state, siteSettings, settings);
|
|
if (button && !button.id) {
|
|
button.id = name;
|
|
}
|
|
return button;
|
|
}
|
|
}
|
|
|
|
function likeCount(attrs) {
|
|
const count = attrs.likeCount;
|
|
|
|
if (count > 0) {
|
|
const title = attrs.liked
|
|
? count === 1
|
|
? "post.has_likes_title_only_you"
|
|
: "post.has_likes_title_you"
|
|
: "post.has_likes_title";
|
|
let icon = attrs.yours ? "d-liked" : "";
|
|
let addContainer = attrs.yours;
|
|
const additionalClass = attrs.yours ? "my-likes" : "regular-likes";
|
|
|
|
if (!attrs.showLike) {
|
|
icon = attrs.yours ? "d-liked" : "d-unliked";
|
|
addContainer = true;
|
|
}
|
|
|
|
return {
|
|
action: "toggleWhoLiked",
|
|
title,
|
|
className: `button-count like-count highlight-action ${additionalClass}`,
|
|
contents: count,
|
|
icon,
|
|
iconRight: true,
|
|
addContainer,
|
|
titleOptions: { count: attrs.liked ? count - 1 : count }
|
|
};
|
|
}
|
|
}
|
|
|
|
registerButton("like-count", likeCount);
|
|
|
|
registerButton("like", attrs => {
|
|
if (!attrs.showLike) {
|
|
return likeCount(attrs);
|
|
}
|
|
|
|
const className = attrs.liked
|
|
? "toggle-like has-like fade-out"
|
|
: "toggle-like like";
|
|
|
|
const button = {
|
|
action: "like",
|
|
icon: attrs.liked ? "d-liked" : "d-unliked",
|
|
className,
|
|
before: "like-count"
|
|
};
|
|
|
|
// If the user has already liked the post and doesn't have permission
|
|
// to undo that operation, then indicate via the title that they've liked it
|
|
// and disable the button. Otherwise, set the title even if the user
|
|
// is anonymous (meaning they don't currently have permission to like);
|
|
// this is important for accessibility.
|
|
if (attrs.liked && !attrs.canToggleLike) {
|
|
button.title = "post.controls.has_liked";
|
|
button.disabled = true;
|
|
} else {
|
|
button.title = attrs.liked
|
|
? "post.controls.undo_like"
|
|
: "post.controls.like";
|
|
}
|
|
|
|
return button;
|
|
});
|
|
|
|
registerButton("flag-count", attrs => {
|
|
let className = "button-count";
|
|
if (attrs.reviewableScorePendingCount > 0) {
|
|
className += " has-pending";
|
|
}
|
|
return {
|
|
className,
|
|
contents: h("span", attrs.reviewableScoreCount.toString()),
|
|
url: `/review/${attrs.reviewableId}`
|
|
};
|
|
});
|
|
|
|
registerButton("flag", attrs => {
|
|
if (attrs.reviewableId || (attrs.canFlag && !attrs.hidden)) {
|
|
let button = {
|
|
action: "showFlags",
|
|
title: "post.controls.flag",
|
|
icon: "flag",
|
|
className: "create-flag"
|
|
};
|
|
if (attrs.reviewableId) {
|
|
button.before = "flag-count";
|
|
}
|
|
return button;
|
|
}
|
|
});
|
|
|
|
registerButton("edit", attrs => {
|
|
if (attrs.canEdit) {
|
|
return {
|
|
action: "editPost",
|
|
className: "edit",
|
|
title: "post.controls.edit",
|
|
icon: "pencil-alt",
|
|
alwaysShowYours: true
|
|
};
|
|
}
|
|
});
|
|
|
|
registerButton("reply-small", attrs => {
|
|
if (!attrs.canCreatePost) {
|
|
return;
|
|
}
|
|
|
|
const args = {
|
|
action: "replyToPost",
|
|
title: "post.controls.reply",
|
|
icon: "reply",
|
|
className: "reply"
|
|
};
|
|
|
|
return args;
|
|
});
|
|
|
|
registerButton("wiki-edit", attrs => {
|
|
if (attrs.canEdit) {
|
|
const args = {
|
|
action: "editPost",
|
|
className: "edit create",
|
|
title: "post.controls.edit",
|
|
icon: "far-edit",
|
|
alwaysShowYours: true
|
|
};
|
|
if (!attrs.mobileView) {
|
|
args.label = "post.controls.edit_action";
|
|
}
|
|
return args;
|
|
}
|
|
});
|
|
|
|
registerButton("replies", (attrs, state, siteSettings) => {
|
|
const replyCount = attrs.replyCount;
|
|
|
|
if (!replyCount) {
|
|
return;
|
|
}
|
|
|
|
// Omit replies if the setting `suppress_reply_directly_below` is enabled
|
|
if (
|
|
replyCount === 1 &&
|
|
attrs.replyDirectlyBelow &&
|
|
siteSettings.suppress_reply_directly_below
|
|
) {
|
|
return;
|
|
}
|
|
|
|
return {
|
|
action: "toggleRepliesBelow",
|
|
className: "show-replies",
|
|
icon: state.repliesShown ? "chevron-up" : "chevron-down",
|
|
titleOptions: { count: replyCount },
|
|
title: "post.has_replies",
|
|
labelOptions: { count: replyCount },
|
|
label: "post.has_replies",
|
|
iconRight: true
|
|
};
|
|
});
|
|
|
|
registerButton("share", attrs => {
|
|
return {
|
|
action: "share",
|
|
className: "share",
|
|
title: "post.controls.share",
|
|
icon: "link",
|
|
data: {
|
|
"share-url": attrs.shareUrl,
|
|
"post-number": attrs.post_number
|
|
}
|
|
};
|
|
});
|
|
|
|
registerButton("reply", (attrs, state, siteSettings, postMenuSettings) => {
|
|
const args = {
|
|
action: "replyToPost",
|
|
title: "post.controls.reply",
|
|
icon: "reply",
|
|
className: "reply create fade-out"
|
|
};
|
|
|
|
if (!attrs.canCreatePost) {
|
|
return;
|
|
}
|
|
|
|
if (postMenuSettings.showReplyTitleOnMobile || !attrs.mobileView) {
|
|
args.label = "topic.reply.title";
|
|
}
|
|
|
|
return args;
|
|
});
|
|
|
|
registerButton("bookmark", attrs => {
|
|
if (!attrs.canBookmark) {
|
|
return;
|
|
}
|
|
|
|
let className = "bookmark";
|
|
|
|
if (attrs.bookmarked) {
|
|
className += " bookmarked";
|
|
}
|
|
|
|
return {
|
|
id: attrs.bookmarked ? "unbookmark" : "bookmark",
|
|
action: "toggleBookmark",
|
|
title: attrs.bookmarked ? "bookmarks.created" : "bookmarks.not_bookmarked",
|
|
className,
|
|
icon: "bookmark"
|
|
};
|
|
});
|
|
|
|
registerButton("admin", attrs => {
|
|
if (!attrs.canManage && !attrs.canWiki) {
|
|
return;
|
|
}
|
|
return {
|
|
action: "openAdminMenu",
|
|
title: "post.controls.admin",
|
|
className: "show-post-admin-menu",
|
|
icon: "wrench"
|
|
};
|
|
});
|
|
|
|
registerButton("delete", attrs => {
|
|
if (attrs.canRecoverTopic) {
|
|
return {
|
|
id: "recover_topic",
|
|
action: "recoverPost",
|
|
title: "topic.actions.recover",
|
|
icon: "undo",
|
|
className: "recover"
|
|
};
|
|
} else if (attrs.canDeleteTopic) {
|
|
return {
|
|
id: "delete_topic",
|
|
action: "deletePost",
|
|
title: "post.controls.delete_topic",
|
|
icon: "far-trash-alt",
|
|
className: "delete"
|
|
};
|
|
} else if (attrs.canRecover) {
|
|
return {
|
|
id: "recover",
|
|
action: "recoverPost",
|
|
title: "post.controls.undelete",
|
|
icon: "undo",
|
|
className: "recover"
|
|
};
|
|
} else if (attrs.canDelete) {
|
|
return {
|
|
id: "delete",
|
|
action: "deletePost",
|
|
title: "post.controls.delete",
|
|
icon: "far-trash-alt",
|
|
className: "delete"
|
|
};
|
|
} else if (attrs.showFlagDelete) {
|
|
return {
|
|
id: "delete_topic",
|
|
action: "showDeleteTopicModal",
|
|
title: "post.controls.delete_topic_disallowed",
|
|
icon: "far-trash-alt",
|
|
className: "delete"
|
|
};
|
|
}
|
|
});
|
|
|
|
function replaceButton(buttons, find, replace) {
|
|
const idx = buttons.indexOf(find);
|
|
if (idx !== -1) {
|
|
buttons[idx] = replace;
|
|
}
|
|
}
|
|
|
|
export default createWidget("post-menu", {
|
|
tagName: "section.post-menu-area.clearfix",
|
|
|
|
settings: {
|
|
collapseButtons: true,
|
|
buttonType: "flat-button",
|
|
showReplyTitleOnMobile: false
|
|
},
|
|
|
|
defaultState() {
|
|
return { collapsed: true, likedUsers: [], adminVisible: false };
|
|
},
|
|
|
|
buildKey: attrs => `post-menu-${attrs.id}`,
|
|
|
|
attachButton(name) {
|
|
let buttonAtts = buildButton(name, this);
|
|
if (buttonAtts) {
|
|
let button = this.attach(this.settings.buttonType, buttonAtts);
|
|
if (buttonAtts.before) {
|
|
let before = this.attachButton(buttonAtts.before);
|
|
return h("div.double-button", [before, button]);
|
|
} else if (buttonAtts.addContainer) {
|
|
return h("div.double-button", [button]);
|
|
}
|
|
|
|
return button;
|
|
}
|
|
},
|
|
|
|
menuItems() {
|
|
let result = this.siteSettings.post_menu.split("|");
|
|
return result;
|
|
},
|
|
|
|
html(attrs, state) {
|
|
const { currentUser, keyValueStore, siteSettings } = this;
|
|
|
|
const hiddenSetting = siteSettings.post_menu_hidden_items || "";
|
|
const hiddenButtons = hiddenSetting
|
|
.split("|")
|
|
.filter(s => !attrs.bookmarked || s !== "bookmark");
|
|
|
|
if (currentUser && keyValueStore) {
|
|
const likedPostId = keyValueStore.getInt("likedPostId");
|
|
if (likedPostId === attrs.id) {
|
|
keyValueStore.remove("likedPostId");
|
|
Ember.run.next(() => this.sendWidgetAction("toggleLike"));
|
|
}
|
|
}
|
|
|
|
const allButtons = [];
|
|
let visibleButtons = [];
|
|
|
|
const orderedButtons = this.menuItems();
|
|
|
|
// If the post is a wiki, make Edit more prominent
|
|
if (attrs.wiki && attrs.canEdit) {
|
|
replaceButton(orderedButtons, "edit", "reply-small");
|
|
replaceButton(orderedButtons, "reply", "wiki-edit");
|
|
}
|
|
|
|
orderedButtons.forEach(i => {
|
|
const button = this.attachButton(i, attrs);
|
|
|
|
if (button) {
|
|
allButtons.push(button);
|
|
|
|
if (
|
|
(attrs.yours && button.attrs && button.attrs.alwaysShowYours) ||
|
|
(attrs.reviewableId && i === "flag") ||
|
|
hiddenButtons.indexOf(i) === -1
|
|
) {
|
|
visibleButtons.push(button);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!this.settings.collapseButtons) {
|
|
visibleButtons = allButtons;
|
|
}
|
|
|
|
// Only show ellipsis if there is more than one button hidden
|
|
// if there are no more buttons, we are not collapsed
|
|
if (!state.collapsed || allButtons.length <= visibleButtons.length + 1) {
|
|
visibleButtons = allButtons;
|
|
if (state.collapsed) {
|
|
state.collapsed = false;
|
|
}
|
|
} else {
|
|
const showMore = this.attach("flat-button", {
|
|
action: "showMoreActions",
|
|
title: "show_more",
|
|
className: "show-more-actions",
|
|
icon: "ellipsis-h"
|
|
});
|
|
visibleButtons.splice(visibleButtons.length - 1, 0, showMore);
|
|
}
|
|
|
|
Object.values(_extraButtons).forEach(builder => {
|
|
if (builder) {
|
|
const buttonAtts = builder(attrs, this.state, this.siteSettings);
|
|
if (buttonAtts) {
|
|
const { position, beforeButton, afterButton } = buttonAtts;
|
|
delete buttonAtts.position;
|
|
|
|
let button = this.attach(this.settings.buttonType, buttonAtts);
|
|
|
|
const content = [];
|
|
if (beforeButton) {
|
|
content.push(beforeButton(h));
|
|
}
|
|
content.push(button);
|
|
if (afterButton) {
|
|
content.push(afterButton(h));
|
|
}
|
|
button = h("span.extra-buttons", content);
|
|
|
|
if (button) {
|
|
switch (position) {
|
|
case "first":
|
|
visibleButtons.unshift(button);
|
|
break;
|
|
case "second":
|
|
visibleButtons.splice(1, 0, button);
|
|
break;
|
|
case "second-last-hidden":
|
|
if (!state.collapsed) {
|
|
visibleButtons.splice(visibleButtons.length - 2, 0, button);
|
|
}
|
|
break;
|
|
default:
|
|
visibleButtons.push(button);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const postControls = [];
|
|
|
|
const repliesButton = this.attachButton("replies", attrs);
|
|
if (repliesButton) {
|
|
postControls.push(repliesButton);
|
|
}
|
|
|
|
let extraControls = applyDecorators(this, "extra-controls", attrs, state);
|
|
postControls.push(h("div.actions", visibleButtons.concat(extraControls)));
|
|
if (state.adminVisible) {
|
|
postControls.push(this.attach("post-admin-menu", attrs));
|
|
}
|
|
|
|
const contents = [
|
|
h(
|
|
"nav.post-controls.clearfix" +
|
|
(this.state.collapsed ? ".collapsed" : ".expanded"),
|
|
postControls
|
|
)
|
|
];
|
|
|
|
if (state.likedUsers.length) {
|
|
const remaining = state.total - state.likedUsers.length;
|
|
contents.push(
|
|
this.attach("small-user-list", {
|
|
users: state.likedUsers,
|
|
addSelf: attrs.liked && remaining === 0,
|
|
listClassName: "who-liked",
|
|
description:
|
|
remaining > 0
|
|
? "post.actions.people.like_capped"
|
|
: "post.actions.people.like",
|
|
count: remaining
|
|
})
|
|
);
|
|
}
|
|
|
|
return contents;
|
|
},
|
|
|
|
openAdminMenu() {
|
|
this.state.adminVisible = true;
|
|
},
|
|
|
|
closeAdminMenu() {
|
|
this.state.adminVisible = false;
|
|
},
|
|
|
|
showDeleteTopicModal() {
|
|
showModal("delete-topic-disallowed");
|
|
},
|
|
|
|
showMoreActions() {
|
|
this.state.collapsed = false;
|
|
if (!this.state.likedUsers.length) {
|
|
return this.getWhoLiked();
|
|
}
|
|
},
|
|
|
|
like() {
|
|
const { attrs, currentUser, keyValueStore } = this;
|
|
|
|
if (!currentUser) {
|
|
keyValueStore &&
|
|
keyValueStore.set({ key: "likedPostId", value: attrs.id });
|
|
return this.sendWidgetAction("showLogin");
|
|
}
|
|
|
|
if (attrs.liked) {
|
|
return this.sendWidgetAction("toggleLike");
|
|
}
|
|
|
|
const $heart = $(`[data-post-id=${attrs.id}] .toggle-like .d-icon`);
|
|
$heart.closest("button").addClass("has-like");
|
|
|
|
const scale = [1.0, 1.5];
|
|
return new Ember.RSVP.Promise(resolve => {
|
|
animateHeart($heart, scale[0], scale[1], () => {
|
|
animateHeart($heart, scale[1], scale[0], () => {
|
|
this.sendWidgetAction("toggleLike").then(() => resolve());
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
refreshLikes() {
|
|
if (this.state.likedUsers.length) {
|
|
return this.getWhoLiked();
|
|
}
|
|
},
|
|
|
|
getWhoLiked() {
|
|
const { attrs, state } = this;
|
|
|
|
return this.store
|
|
.find("post-action-user", {
|
|
id: attrs.id,
|
|
post_action_type_id: LIKE_ACTION
|
|
})
|
|
.then(users => {
|
|
state.likedUsers = users.map(avatarAtts);
|
|
state.total = users.totalRows;
|
|
});
|
|
},
|
|
|
|
toggleWhoLiked() {
|
|
const state = this.state;
|
|
if (state.likedUsers.length) {
|
|
state.likedUsers = [];
|
|
} else {
|
|
return this.getWhoLiked();
|
|
}
|
|
}
|
|
});
|