mirror of
https://github.com/discourse/discourse.git
synced 2025-02-17 07:02:48 +08:00
FEATURE: Add copy link post menu button (#24709)
This commit ports the feature by @chapoi that was previously a theme component in core. A new post_menu button, copyLink, is added and used as the default instead of share. copyLink, on desktop, will copy the link of the post to the user's clipboard and show a nice 'lil animation. On mobile the native share menu will be shown. If site owners want the old behaviour back, they just need to change the post_menu site setting to use the share button instead of copyLink.
This commit is contained in:
parent
5b91dc1844
commit
d5fe9b4f8c
|
@ -5,7 +5,7 @@ import deprecated from "discourse-common/lib/deprecated";
|
|||
import escape from "discourse-common/lib/escape";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
||||
export const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
||||
let _renderers = [];
|
||||
|
||||
let warnMissingIcons = true;
|
||||
|
|
64
app/assets/javascripts/discourse/app/lib/copy-post-link.js
Normal file
64
app/assets/javascripts/discourse/app/lib/copy-post-link.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { SVG_NAMESPACE } from "discourse-common/lib/icon-library";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export function recentlyCopiedPostLink(postId) {
|
||||
return document.querySelector(
|
||||
`article[data-post-id='${postId}'] .post-action-menu__copy-link .post-action-menu__copy-link-checkmark`
|
||||
);
|
||||
}
|
||||
|
||||
export function showCopyPostLinkAlert(postId) {
|
||||
const postSelector = `article[data-post-id='${postId}']`;
|
||||
const copyLinkBtn = document.querySelector(
|
||||
`${postSelector} .post-action-menu__copy-link`
|
||||
);
|
||||
createAlert(I18n.t("post.controls.link_copied"), postId, copyLinkBtn);
|
||||
createCheckmark(copyLinkBtn, postId);
|
||||
styleLinkBtn(copyLinkBtn);
|
||||
}
|
||||
|
||||
function createAlert(message, postId, copyLinkBtn) {
|
||||
if (!copyLinkBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
let alertDiv = document.createElement("div");
|
||||
alertDiv.className = "post-link-copied-alert -success";
|
||||
alertDiv.textContent = message;
|
||||
|
||||
copyLinkBtn.appendChild(alertDiv);
|
||||
|
||||
setTimeout(() => alertDiv.classList.add("slide-out"), 1000);
|
||||
setTimeout(() => removeElement(alertDiv), 2500);
|
||||
}
|
||||
|
||||
function createCheckmark(btn, postId) {
|
||||
const checkmark = makeCheckmarkSvg(postId);
|
||||
btn.appendChild(checkmark.content);
|
||||
|
||||
setTimeout(() => checkmark.classList.remove("is-visible"), 3000);
|
||||
setTimeout(
|
||||
() =>
|
||||
removeElement(document.querySelector(`#copy_post_svg_postId_${postId}`)),
|
||||
3500
|
||||
);
|
||||
}
|
||||
|
||||
function styleLinkBtn(copyLinkBtn) {
|
||||
copyLinkBtn.classList.add("is-copied");
|
||||
setTimeout(() => copyLinkBtn.classList.remove("is-copied"), 3200);
|
||||
}
|
||||
|
||||
function makeCheckmarkSvg(postId) {
|
||||
const svgElement = document.createElement("template");
|
||||
svgElement.innerHTML = `
|
||||
<svg class="post-action-menu__copy-link-checkmark is-visible" id="copy_post_svg_postId_${postId}" xmlns="${SVG_NAMESPACE}" viewBox="0 0 52 52">
|
||||
<path class="checkmark__check" fill="none" d="M13 26 l10 10 20 -20"/>
|
||||
</svg>
|
||||
`;
|
||||
return svgElement;
|
||||
}
|
||||
|
||||
function removeElement(element) {
|
||||
element?.parentNode?.removeChild(element);
|
||||
}
|
|
@ -331,9 +331,18 @@ registerButton("replies", (attrs, state, siteSettings) => {
|
|||
registerButton("share", () => {
|
||||
return {
|
||||
action: "share",
|
||||
icon: "d-post-share",
|
||||
className: "share",
|
||||
title: "post.controls.share",
|
||||
};
|
||||
});
|
||||
|
||||
registerButton("copyLink", () => {
|
||||
return {
|
||||
action: "copyLink",
|
||||
icon: "d-post-share",
|
||||
className: "post-action-menu__copy-link",
|
||||
title: "post.controls.copy_title",
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@ import { h } from "virtual-dom";
|
|||
import ShareTopicModal from "discourse/components/modal/share-topic";
|
||||
import { dateNode } from "discourse/helpers/node";
|
||||
import autoGroupFlairForUser from "discourse/lib/avatar-flair";
|
||||
import {
|
||||
recentlyCopiedPostLink,
|
||||
showCopyPostLinkAlert,
|
||||
} from "discourse/lib/copy-post-link";
|
||||
import { relativeAgeMediumSpan } from "discourse/lib/formatter";
|
||||
import { nativeShare } from "discourse/lib/pwa-utils";
|
||||
import {
|
||||
|
@ -12,13 +16,14 @@ import {
|
|||
} from "discourse/lib/settings";
|
||||
import { transformBasicPost } from "discourse/lib/transform-post";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { formatUsername } from "discourse/lib/utilities";
|
||||
import { clipboardCopy, formatUsername } from "discourse/lib/utilities";
|
||||
import DecoratorHelper from "discourse/widgets/decorator-helper";
|
||||
import hbs from "discourse/widgets/hbs-compiler";
|
||||
import PostCooked from "discourse/widgets/post-cooked";
|
||||
import { postTransformCallbacks } from "discourse/widgets/post-stream";
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import { applyDecorators, createWidget } from "discourse/widgets/widget";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { avatarUrl, translateSize } from "discourse-common/lib/avatar-utils";
|
||||
import getURL, { getURLWithCDN } from "discourse-common/lib/get-url";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
|
@ -642,6 +647,38 @@ createWidget("post-contents", {
|
|||
});
|
||||
},
|
||||
|
||||
copyLink() {
|
||||
// Copying the link to clipboard on mobile doesn't make sense.
|
||||
if (this.site.mobileView) {
|
||||
return this.share();
|
||||
}
|
||||
|
||||
const post = this.findAncestorModel();
|
||||
const postUrl = post.shareUrl;
|
||||
const postId = post.id;
|
||||
|
||||
// Do nothing if the user just copied the link.
|
||||
if (recentlyCopiedPostLink(postId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shareUrl = new URL(postUrl, window.origin).toString();
|
||||
|
||||
// Can't use clipboard in JS tests.
|
||||
if (isTesting()) {
|
||||
return showCopyPostLinkAlert(postId);
|
||||
}
|
||||
|
||||
clipboardCopy(shareUrl)
|
||||
.then(() => {
|
||||
showCopyPostLinkAlert(postId);
|
||||
})
|
||||
.catch(() => {
|
||||
// If the clipboard copy fails for some reason, may as well show the old modal.
|
||||
this.share();
|
||||
});
|
||||
},
|
||||
|
||||
init() {
|
||||
this.postContentsDestroyCallbacks = [];
|
||||
},
|
||||
|
|
|
@ -25,6 +25,9 @@ import I18n from "discourse-i18n";
|
|||
|
||||
acceptance("Topic", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({
|
||||
post_menu: "read|like|share|flag|edit|bookmark|delete|admin|reply|copyLink",
|
||||
});
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/c/2/visible_groups.json", () =>
|
||||
helper.response(200, {
|
||||
|
@ -87,6 +90,16 @@ acceptance("Topic", function (needs) {
|
|||
assert.ok(exists(".share-topic-modal"), "it shows the share modal");
|
||||
});
|
||||
|
||||
test("Copy Link Button", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(".topic-post:first-child button.post-action-menu__copy-link");
|
||||
|
||||
assert.ok(
|
||||
exists(".post-action-menu__copy-link-checkmark"),
|
||||
"it shows the Link Copied! message"
|
||||
);
|
||||
});
|
||||
|
||||
test("Showing and hiding the edit controls", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
|
|
|
@ -192,7 +192,7 @@ module("Integration | Component | Widget | post", function (hooks) {
|
|||
assert.ok(!exists(".who-liked a.trigger-user-card"));
|
||||
});
|
||||
|
||||
test(`like count with no likes`, async function (assert) {
|
||||
test("like count with no likes", async function (assert) {
|
||||
this.set("args", { likeCount: 0 });
|
||||
|
||||
await render(
|
||||
|
@ -203,6 +203,7 @@ module("Integration | Component | Widget | post", function (hooks) {
|
|||
});
|
||||
|
||||
test("share button", async function (assert) {
|
||||
this.siteSettings.post_menu += "|share";
|
||||
this.set("args", { shareUrl: "http://share-me.example.com" });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
@ -210,6 +211,17 @@ module("Integration | Component | Widget | post", function (hooks) {
|
|||
assert.ok(exists(".actions button.share"), "it renders a share button");
|
||||
});
|
||||
|
||||
test("copy link button", async function (assert) {
|
||||
this.set("args", { shareUrl: "http://share-me.example.com" });
|
||||
|
||||
await render(hbs`<MountWidget @widget="post" @args={{this.args}} />`);
|
||||
|
||||
assert.ok(
|
||||
exists(".actions button.post-action-menu__copy-link"),
|
||||
"it renders a copy link button"
|
||||
);
|
||||
});
|
||||
|
||||
test("liking", async function (assert) {
|
||||
const args = { showLike: true, canToggleLike: true, id: 5 };
|
||||
this.set("args", args);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
@import "modal";
|
||||
@import "topic-list";
|
||||
@import "topic-post";
|
||||
@import "post-action-menu";
|
||||
@import "topic";
|
||||
@import "upload";
|
||||
@import "user";
|
||||
|
|
76
app/assets/stylesheets/desktop/post-action-menu.scss
Normal file
76
app/assets/stylesheets/desktop/post-action-menu.scss
Normal file
|
@ -0,0 +1,76 @@
|
|||
@keyframes slide {
|
||||
0% {
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100%) translateX(-50%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.post-link-copied-alert {
|
||||
position: absolute;
|
||||
top: -1.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: var(--success);
|
||||
padding: 0.25rem 0.5rem;
|
||||
white-space: nowrap;
|
||||
font-size: var(--font-down-2);
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
z-index: z("modal", "popover");
|
||||
&.-success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.-fail {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
&.slide-out {
|
||||
animation: slide 1s cubic-bezier(0, 0, 0, 2) forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes draw {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.post-action-menu {
|
||||
&__copy-link {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&.is-copied,
|
||||
&.is-copied:hover {
|
||||
.d-icon-d-post-share {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
&__copy-link-checkmark {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
stroke: #2ecc71;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
&.is-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
path {
|
||||
stroke: var(--success);
|
||||
stroke-width: 4;
|
||||
stroke-dasharray: 100;
|
||||
stroke-dashoffset: 100;
|
||||
animation: draw 1s forwards;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3553,6 +3553,8 @@ en:
|
|||
delete: "delete this post"
|
||||
undelete: "undelete this post"
|
||||
share: "share a link to this post"
|
||||
copy_title: "copy a link to this post to clipboard"
|
||||
link_copied: "Link copied!"
|
||||
more: "More"
|
||||
delete_replies:
|
||||
confirm: "Do you also want to delete the replies to this post?"
|
||||
|
|
|
@ -193,15 +193,16 @@ basic:
|
|||
client: true
|
||||
type: list
|
||||
list_type: simple
|
||||
default: "read|like|share|flag|edit|bookmark|delete|admin|reply"
|
||||
default: "read|like|copyLink|flag|edit|bookmark|delete|admin|reply"
|
||||
allow_any: false
|
||||
choices:
|
||||
- read
|
||||
- copyLink
|
||||
- share
|
||||
- like
|
||||
- edit
|
||||
- flag
|
||||
- delete
|
||||
- share
|
||||
- bookmark
|
||||
- admin
|
||||
- reply
|
||||
|
|
Loading…
Reference in New Issue
Block a user