This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/widgets/post-menu.js
Martin Brennan b8b29e79ad
FIX: Improve user timezone saving (#9230)
Based on issues identified in https://meta.discourse.org/t/improved-bookmarks-with-reminders/144542/20

* Implement the resolvedTimezone() function on the user model where we return the user's timezone if it has been set, or we guess it using moment and save it to the user using an update call if it has not yet been set. This covers the cases of users who do not log out/in often who will not get their timezone set via login. This also makes sure the guess + save is done in a non-obtrusive way not on every page -- only when it is needed.

* Before if a user's timezone was blank when they visited their profile page we were autofilling the dropdown with the guessed timezone from moment. However this was confusing as it would appear you have that timezone saved in the DB when you really didn't. Now we do not autofill the dropdown and added a button to automatically guess the current timezone to make everything more explicit.
2020-03-24 11:39:09 +10:00

733 lines
18 KiB
JavaScript

import { next, run } from "@ember/runloop";
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";
import { Promise } from "rsvp";
import ENV from "discourse-common/config/environment";
const LIKE_ACTION = 2;
const VIBRATE_DURATION = 5;
function animateHeart($elem, start, end, complete) {
if (ENV.environment === "test") {
return 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;
}
}
registerButton("read-count", attrs => {
if (attrs.showReadIndicator) {
const count = attrs.readCount;
if (count > 0) {
return {
action: "toggleWhoRead",
title: "post.controls.read_indicator",
className: "button-count read-indicator",
contents: count,
iconRight: true,
addContainer: false
};
}
}
});
registerButton("read", attrs => {
const readBySomeone = attrs.readCount > 0;
if (attrs.showReadIndicator && readBySomeone) {
return {
action: "toggleWhoRead",
title: "post.controls.read_indicator",
icon: "book-reader",
before: "read-count",
addContainer: false
};
}
});
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("bookmarkWithReminder", (attrs, state, siteSettings) => {
if (!attrs.canBookmark || !siteSettings.enable_bookmarks_with_reminders) {
return;
}
let classNames = ["bookmark", "with-reminder"];
let title = "bookmarks.not_bookmarked";
let titleOptions = {};
if (attrs.bookmarkedWithReminder) {
classNames.push("bookmarked");
if (attrs.bookmarkReminderAt) {
let reminderAtDate = moment(attrs.bookmarkReminderAt).tz(
Discourse.currentUser.resolvedTimezone()
);
title = "bookmarks.created_with_reminder";
titleOptions = {
date: reminderAtDate.format(I18n.t("dates.long_with_year"))
};
} else if (attrs.bookmarkReminderType === "at_desktop") {
title = "bookmarks.created_with_at_desktop_reminder";
} else {
title = "bookmarks.created";
}
}
return {
id: attrs.bookmarkedWithReminder ? "unbookmark" : "bookmark",
action: "toggleBookmarkWithReminder",
title,
titleOptions,
className: classNames.join(" "),
icon:
attrs.bookmarkReminderAt || attrs.bookmarkReminderType === "at_desktop"
? "discourse-bookmark-clock"
: "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: [],
readers: [],
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() {
return this.siteSettings.post_menu.split("|").filter(Boolean);
},
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")
.filter(
s => !attrs.bookmarkedWithReminder || s !== "bookmarkWithReminder"
);
if (currentUser && keyValueStore) {
const likedPostId = keyValueStore.getInt("likedPostId");
if (likedPostId === attrs.id) {
keyValueStore.remove("likedPostId");
next(() => this.sendWidgetAction("toggleLike"));
}
}
const allButtons = [];
let visibleButtons = [];
// filter menu items based on site settings
const orderedButtons = this.menuItems().filter(button => {
if (
this.siteSettings.enable_bookmarks_with_reminders &&
button === "bookmark"
) {
return false;
}
return true;
});
// 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.readers.length) {
const remaining = state.totalReaders - state.readers.length;
const description =
remaining > 0
? "post.actions.people.read_capped"
: "post.actions.people.read";
const count = remaining > 0 ? remaining : state.totalReaders;
contents.push(
this.attach("small-user-list", {
users: state.readers,
addSelf: false,
listClassName: "who-read",
description,
count
})
);
}
if (state.likedUsers.length) {
const remaining = state.total - state.likedUsers.length;
const description =
remaining > 0
? "post.actions.people.like_capped"
: "post.actions.people.like";
const count = remaining > 0 ? remaining : state.total;
contents.push(
this.attach("small-user-list", {
users: state.likedUsers,
addSelf: attrs.liked && remaining === 0,
listClassName: "who-liked",
description,
count
})
);
}
return contents;
},
openAdminMenu() {
this.state.adminVisible = true;
},
closeAdminMenu() {
this.state.adminVisible = false;
},
showDeleteTopicModal() {
showModal("delete-topic-disallowed");
},
showMoreActions() {
this.state.collapsed = false;
const likesPromise = !this.state.likedUsers.length
? this.getWhoLiked()
: Promise.resolve();
return likesPromise.then(() => {
if (!this.state.readers.length && this.attrs.showReadIndicator) {
return this.getWhoRead();
}
});
},
like() {
const { attrs, currentUser, keyValueStore } = this;
if (!currentUser) {
keyValueStore &&
keyValueStore.set({ key: "likedPostId", value: attrs.id });
return this.sendWidgetAction("showLogin");
}
if (this.capabilities.canVibrate) {
navigator.vibrate(VIBRATE_DURATION);
}
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 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();
}
},
refreshReaders() {
if (this.state.readers.length) {
return this.getWhoRead();
}
},
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;
});
},
getWhoRead() {
const { attrs, state } = this;
return this.store.find("post-reader", { id: attrs.id }).then(users => {
state.readers = users.map(avatarAtts);
state.totalReaders = users.totalRows;
});
},
toggleWhoLiked() {
const state = this.state;
if (state.likedUsers.length) {
state.likedUsers = [];
} else {
return this.getWhoLiked();
}
},
toggleWhoRead() {
const state = this.state;
if (this.state.readers.length) {
state.readers = [];
} else {
return this.getWhoRead();
}
}
});