diff --git a/app/assets/javascripts/discourse/components/share-panel.js.es6 b/app/assets/javascripts/discourse/components/share-panel.js.es6
index 234e8d70a24..18a48011145 100644
--- a/app/assets/javascripts/discourse/components/share-panel.js.es6
+++ b/app/assets/javascripts/discourse/components/share-panel.js.es6
@@ -1,15 +1,12 @@
import { escapeExpression } from "discourse/lib/utilities";
-import { longDateNoYear } from "discourse/lib/formatter";
import { default as computed } from "ember-addons/ember-computed-decorators";
import Sharing from "discourse/lib/sharing";
export default Ember.Component.extend({
tagName: null,
- date: Ember.computed.alias("panel.model.date"),
type: Ember.computed.alias("panel.model.type"),
- postNumber: Ember.computed.alias("panel.model.postNumber"),
- postId: Ember.computed.alias("panel.model.postId"),
+
topic: Ember.computed.alias("panel.model.topic"),
@computed
@@ -17,21 +14,9 @@ export default Ember.Component.extend({
return Sharing.activeSources(this.siteSettings.share_links);
},
- @computed("date")
- postDate(date) {
- return date ? longDateNoYear(new Date(date)) : null;
- },
-
- @computed("type", "postNumber", "postDate", "topic.title")
- shareTitle(type, postNumber, postDate, topicTitle) {
+ @computed("type", "topic.title")
+ shareTitle(type, topicTitle) {
topicTitle = escapeExpression(topicTitle);
-
- if (type === "topic") {
- return I18n.t("share.topic_html", { topicTitle });
- }
- if (postNumber) {
- return I18n.t("share.post_html", { postNumber, postDate });
- }
return I18n.t("share.topic_html", { topicTitle });
},
@@ -81,27 +66,10 @@ export default Ember.Component.extend({
actions: {
share(source) {
- const url = source.generateUrl(
- this.get("shareUrl"),
- this.get("topic.title")
- );
- const options = {
- menubar: "no",
- toolbar: "no",
- resizable: "yes",
- scrollbars: "yes",
- width: 600,
- height: source.popupHeight || 315
- };
- const stringOptions = Object.keys(options)
- .map(k => `${k}=${options[k]}`)
- .join(",");
-
- if (source.shouldOpenInPopup) {
- window.open(url, "", stringOptions);
- } else {
- window.open(url, "_blank");
- }
+ Sharing.shareSource(source, {
+ url: this.get("shareUrl"),
+ title: this.get("topic.title")
+ });
}
}
});
diff --git a/app/assets/javascripts/discourse/components/share-popup.js.es6 b/app/assets/javascripts/discourse/components/share-popup.js.es6
new file mode 100644
index 00000000000..5b0ff9745e9
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/share-popup.js.es6
@@ -0,0 +1,173 @@
+import { wantsNewWindow } from "discourse/lib/intercept-click";
+import { longDateNoYear } from "discourse/lib/formatter";
+import computed from "ember-addons/ember-computed-decorators";
+import Sharing from "discourse/lib/sharing";
+import { nativeShare } from "discourse/lib/pwa-utils";
+
+export default Ember.Component.extend({
+ elementId: "share-link",
+ classNameBindings: ["visible"],
+ link: null,
+ visible: null,
+
+ @computed
+ sources() {
+ return Sharing.activeSources(this.siteSettings.share_links);
+ },
+
+ @computed("type", "postNumber")
+ shareTitle(type, postNumber) {
+ if (type === "topic") {
+ return I18n.t("share.topic");
+ }
+ if (postNumber) {
+ return I18n.t("share.post", { postNumber });
+ }
+ return I18n.t("share.topic");
+ },
+
+ @computed("date")
+ displayDate(date) {
+ return longDateNoYear(new Date(date));
+ },
+
+ _focusUrl() {
+ const link = this.get("link");
+ if (!this.capabilities.touch) {
+ const $linkInput = $("#share-link input");
+ $linkInput.val(link);
+
+ // Wait for the fade-in transition to finish before selecting the link:
+ window.setTimeout(() => $linkInput.select().focus(), 160);
+ } else {
+ const $linkForTouch = $("#share-link .share-for-touch a");
+ $linkForTouch.attr("href", link);
+ $linkForTouch.text(link);
+ const range = window.document.createRange();
+ range.selectNode($linkForTouch[0]);
+ window.getSelection().addRange(range);
+ }
+ },
+
+ _showUrl($target, url) {
+ const $currentTargetOffset = $target.offset();
+ const $this = this.$();
+
+ if (Ember.isEmpty(url)) {
+ return;
+ }
+
+ // Relative urls
+ if (url.indexOf("/") === 0) {
+ url = window.location.protocol + "//" + window.location.host + url;
+ }
+
+ const shareLinkWidth = $this.width();
+ let x = $currentTargetOffset.left - shareLinkWidth / 2;
+ if (x < 25) {
+ x = 25;
+ }
+ if (x + shareLinkWidth > $(window).width()) {
+ x -= shareLinkWidth / 2;
+ }
+
+ const header = $(".d-header");
+ let y = $currentTargetOffset.top - ($this.height() + 20);
+ if (y < header.offset().top + header.height()) {
+ y = $currentTargetOffset.top + 10;
+ }
+
+ $this.css({ top: "" + y + "px" });
+
+ if (!this.site.mobileView) {
+ $this.css({ left: "" + x + "px" });
+ }
+ this.set("link", encodeURI(url));
+ this.set("visible", true);
+
+ Ember.run.scheduleOnce("afterRender", this, this._focusUrl);
+ },
+
+ didInsertElement() {
+ this._super(...arguments);
+
+ const $html = $("html");
+ $html.on("mousedown.outside-share-link", e => {
+ // Use mousedown instead of click so this event is handled before routing occurs when a
+ // link is clicked (which is a click event) while the share dialog is showing.
+ if (this.$().has(e.target).length !== 0) {
+ return;
+ }
+ this.send("close");
+ return true;
+ });
+
+ $html.on(
+ "click.discourse-share-link",
+ "button[data-share-url], .post-info .post-date[data-share-url]",
+ e => {
+ // if they want to open in a new tab, let it so
+ if (wantsNewWindow(e)) {
+ return true;
+ }
+
+ e.preventDefault();
+
+ const $currentTarget = $(e.currentTarget);
+ const url = $currentTarget.data("share-url");
+ const postNumber = $currentTarget.data("post-number");
+ const postId = $currentTarget.closest("article").data("post-id");
+ const date = $currentTarget.children().data("time");
+
+ this.setProperties({ postNumber, date, postId });
+
+ // use native webshare only when the user clicks on the "chain" icon
+ if (!$currentTarget.hasClass("post-date")) {
+ nativeShare({ url }).then(null, () =>
+ this._showUrl($currentTarget, url)
+ );
+ } else {
+ this._showUrl($currentTarget, url);
+ }
+
+ return false;
+ }
+ );
+
+ $html.on("keydown.share-view", e => {
+ if (e.keyCode === 27) {
+ this.send("close");
+ }
+ });
+
+ this.appEvents.on("share:url", (url, $target) =>
+ this._showUrl($target, url)
+ );
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+ $("html")
+ .off("click.discourse-share-link")
+ .off("mousedown.outside-share-link")
+ .off("keydown.share-view");
+ },
+
+ actions: {
+ close() {
+ this.setProperties({
+ link: null,
+ postNumber: null,
+ postId: null,
+ visible: false
+ });
+ },
+
+ share(source) {
+ Sharing.shareSource(source, {
+ url: this.get("link"),
+ title: this.get("topic.title")
+ });
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/lib/sharing.js.es6 b/app/assets/javascripts/discourse/lib/sharing.js.es6
index cdd6ec506e6..4e57b9dfa5e 100644
--- a/app/assets/javascripts/discourse/lib/sharing.js.es6
+++ b/app/assets/javascripts/discourse/lib/sharing.js.es6
@@ -47,6 +47,27 @@ export default {
_sources[source.id] = source;
},
+ shareSource(source, data) {
+ const url = source.generateUrl(data.url, data.title);
+ const options = {
+ menubar: "no",
+ toolbar: "no",
+ resizable: "yes",
+ scrollbars: "yes",
+ width: 600,
+ height: source.popupHeight || 315
+ };
+ const stringOptions = Object.keys(options)
+ .map(k => `${k}=${options[k]}`)
+ .join(",");
+
+ if (source.shouldOpenInPopup) {
+ window.open(url, "", stringOptions);
+ } else {
+ window.open(url, "_blank");
+ }
+ },
+
activeSources(linksSetting = "") {
return linksSetting
.split("|")
diff --git a/app/assets/javascripts/discourse/templates/components/share-popup.hbs b/app/assets/javascripts/discourse/templates/components/share-popup.hbs
new file mode 100644
index 00000000000..84b946fcbbc
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/share-popup.hbs
@@ -0,0 +1,24 @@
+
+
{{{shareTitle}}}
+
+ {{#if date}}
+ {{displayDate}}
+ {{/if}}
+
+
+
+
+
+ {{#each sources as |s|}}
+ {{share-source source=s title=model.title action=(action "share")}}
+ {{/each}}
+
+
+
diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs
index 14b06cf69e4..0c9629ca9d7 100644
--- a/app/assets/javascripts/discourse/templates/topic.hbs
+++ b/app/assets/javascripts/discourse/templates/topic.hbs
@@ -318,6 +318,8 @@
{{/if}}
+ {{share-popup topic=model}}
+
{{#if embedQuoteButton}}
{{quote-button quoteState=quoteState selectText=(action "selectText")}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6
index 6eb11139a5a..5aa17d79c99 100644
--- a/app/assets/javascripts/discourse/widgets/post.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post.js.es6
@@ -2,7 +2,6 @@ import PostCooked from "discourse/widgets/post-cooked";
import DecoratorHelper from "discourse/widgets/decorator-helper";
import { createWidget, applyDecorators } from "discourse/widgets/widget";
import { iconNode } from "discourse-common/lib/icon-library";
-import { nativeShare } from "discourse/lib/pwa-utils";
import { transformBasicPost } from "discourse/lib/transform-post";
import { postTransformCallbacks } from "discourse/widgets/post-stream";
import { h } from "virtual-dom";
@@ -14,7 +13,6 @@ import {
formatUsername
} from "discourse/lib/utilities";
import hbs from "discourse/widgets/hbs-compiler";
-import showModal from "discourse/lib/show-modal";
function transformWithCallbacks(post) {
let transformed = transformBasicPost(post);
@@ -221,72 +219,6 @@ function showReplyTab(attrs, siteSettings) {
);
}
-createWidget("post-date", {
- tagName: "div.post-info.post-date",
-
- buildClasses(attrs) {
- let classes = "post-date";
-
- const lastWikiEdit =
- attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit);
-
- if (lastWikiEdit) {
- classes = `${classes} last-wiki-edit`;
- }
-
- return classes;
- },
-
- html(attrs) {
- return h(
- "a",
- {
- attributes: {
- class: "post-date",
- href: attrs.shareUrl,
- "data-share-url": attrs.shareUrl,
- "data-post-number": attrs.post_number
- }
- },
- dateNode(this._date(attrs))
- );
- },
-
- _date(attrs) {
- const lastWikiEdit =
- attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit);
- const createdAt = new Date(attrs.created_at);
- return lastWikiEdit ? lastWikiEdit : createdAt;
- },
-
- click(event) {
- event.preventDefault();
-
- const post = this.findAncestorModel();
-
- const modalFallback = () => {
- showModal("share-and-invite", {
- modalClass: "share-and-invite",
- panels: [
- {
- id: "share",
- title: "topic.share.extended_title",
- model: {
- postNumber: this.attrs.post_number,
- shareUrl: this.attrs.shareUrl,
- date: this._date(this.attrs),
- postId: post.get("id"),
- topic: post.get("topic")
- }
- }
- ]
- });
- };
-
- nativeShare({ url: this.attrs.shareUrl }).then(null, modalFallback);
- }
-});
-
createWidget("post-meta-data", {
tagName: "div.topic-meta-data",
@@ -309,6 +241,21 @@ createWidget("post-meta-data", {
);
}
+ const lastWikiEdit =
+ attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit);
+ const createdAt = new Date(attrs.created_at);
+ const date = lastWikiEdit ? dateNode(lastWikiEdit) : dateNode(createdAt);
+ const attributes = {
+ class: "post-date",
+ href: attrs.shareUrl,
+ "data-share-url": attrs.shareUrl,
+ "data-post-number": attrs.post_number
+ };
+
+ if (lastWikiEdit) {
+ attributes["class"] += " last-wiki-edit";
+ }
+
if (attrs.via_email) {
postInfo.push(this.attach("post-email-indicator", attrs));
}
@@ -329,7 +276,7 @@ createWidget("post-meta-data", {
postInfo.push(this.attach("reply-to-tab", attrs));
}
- postInfo.push(this.attach("post-date", attrs));
+ postInfo.push(h("div.post-info.post-date", h("a", { attributes }, date)));
postInfo.push(
h(
@@ -451,31 +398,6 @@ createWidget("post-contents", {
return lastWikiEdit ? lastWikiEdit : createdAt;
},
- share() {
- const post = this.findAncestorModel();
-
- const modalFallback = () => {
- showModal("share-and-invite", {
- modalClass: "share-and-invite",
- panels: [
- {
- id: "share",
- title: "topic.share.extended_title",
- model: {
- postNumber: this.attrs.post_number,
- shareUrl: this.attrs.shareUrl,
- date: this._date(this.attrs),
- postId: post.get("id"),
- topic: post.get("topic")
- }
- }
- ]
- });
- };
-
- nativeShare({ url: this.attrs.shareUrl }).then(null, modalFallback);
- },
-
toggleRepliesBelow(goToPost = "false") {
if (this.state.repliesBelow.length) {
this.state.repliesBelow = [];
diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss
index 659b86bb295..4fea95eb2ec 100644
--- a/app/assets/stylesheets/common/base/modal.scss
+++ b/app/assets/stylesheets/common/base/modal.scss
@@ -646,7 +646,9 @@
background: $danger;
&.single-tab {
- display: none;
+ background: none;
+ color: $primary;
+ padding: s(1 0);
}
}
}
diff --git a/app/assets/stylesheets/common/base/share_link.scss b/app/assets/stylesheets/common/base/share_link.scss
new file mode 100644
index 00000000000..3abd01db9e4
--- /dev/null
+++ b/app/assets/stylesheets/common/base/share_link.scss
@@ -0,0 +1,87 @@
+// styles that apply to the "share" popup when sharing a link to a post or topic
+
+#share-link {
+ position: absolute;
+ left: 20px;
+ z-index: z("dropdown");
+ box-shadow: shadow("card");
+ background-color: $secondary;
+ padding: s(2 2 1 2);
+ width: 300px;
+ display: none;
+ &.visible {
+ display: block;
+ }
+ input[type="text"] {
+ width: 100%;
+ }
+ .share-for-touch .overflow-ellipsis {
+ clear: both;
+ }
+ .share-for-touch {
+ margin: 14px 0;
+ }
+
+ .title {
+ margin-bottom: s(1);
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+
+ h3 {
+ font-size: $font-0;
+ margin: 0;
+ }
+
+ .date {
+ font-weight: normal;
+ color: dark-light-choose($primary-medium, $secondary-medium);
+ }
+ }
+
+ .copy-text {
+ display: inline-block;
+ position: absolute;
+ margin: 5px 5px 5px 15px;
+ color: $success;
+ opacity: 1;
+ transition: opacity 0.25s;
+ font-size: $font-0;
+ &:not(.success) {
+ opacity: 0;
+ }
+ }
+ .social-link {
+ margin-right: s(2);
+ font-size: $font-up-4;
+ }
+ .link {
+ font-size: $font-up-3;
+ a {
+ color: dark-light-choose($primary-medium, $secondary-medium);
+ }
+ }
+
+ input[type="text"] {
+ font-size: $font-up-1;
+ margin-bottom: 0;
+ }
+
+ .actions {
+ display: flex;
+ align-items: center;
+ margin-top: s(2);
+
+ .link {
+ margin-left: auto;
+ }
+ }
+}
+
+.discourse-no-touch #share-link .share-for-touch {
+ display: none;
+}
+
+.discourse-touch #share-link input[type="text"] {
+ display: none;
+}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 135cec512dc..9ee306acad0 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -136,7 +136,7 @@ en:
placeholder: date
share:
topic_html: 'Topic: %{topicTitle}'
- post_html: 'Post #%{postNumber}, %{postDate}'
+ post: "post #%{postNumber}"
close: "close"
twitter: "Share this link on Twitter"
facebook: "Share this link on Facebook"
diff --git a/test/javascripts/acceptance/share-and-invite-desktop-test.js.es6 b/test/javascripts/acceptance/share-and-invite-desktop-test.js.es6
index def39dc4b6c..cdca5f7caf2 100644
--- a/test/javascripts/acceptance/share-and-invite-desktop-test.js.es6
+++ b/test/javascripts/acceptance/share-and-invite-desktop-test.js.es6
@@ -68,34 +68,5 @@ QUnit.test("Post date link", async assert => {
await visit("/t/internationalization-localization/280");
await click("#post_2 .post-info.post-date a");
- assert.ok(exists(".share-and-invite.modal"), "it shows the modal");
-
- assert.ok(
- exists(".share-and-invite.modal .modal-tab.share"),
- "it shows the share tab"
- );
-
- assert.ok(
- exists(".share-and-invite.modal .modal-tab.share.single-tab"),
- "it shows only one tab"
- );
-
- assert.ok(
- !exists(".share-and-invite.modal .modal-tab.invite"),
- "it doesn’t show the invite tab"
- );
-
- assert.ok(
- find(".share-and-invite.modal .modal-panel.share .title")
- .text()
- .includes("Post #2, Feb"),
- "it shows the post number with the date"
- );
-
- assert.ok(
- find(".share-and-invite.modal .modal-panel.share .topic-share-url")
- .val()
- .includes("/t/internationalization-localization/280/2?u=eviltrout"),
- "it shows the topic sharing url including the post number"
- );
+ assert.ok(exists("#share-link"), "it shows the share modal");
});
diff --git a/test/javascripts/acceptance/share-and-invite-mobile-test.js.es6 b/test/javascripts/acceptance/share-and-invite-mobile-test.js.es6
index 0bd3a744bea..561ab00ec6d 100644
--- a/test/javascripts/acceptance/share-and-invite-mobile-test.js.es6
+++ b/test/javascripts/acceptance/share-and-invite-mobile-test.js.es6
@@ -57,20 +57,5 @@ QUnit.test("Post date link", async assert => {
await visit("/t/internationalization-localization/280");
await click("#post_2 .post-info.post-date a");
- assert.ok(exists(".share-and-invite.modal"), "it shows the modal");
-
- assert.ok(
- exists(".share-and-invite.modal .modal-tab.share"),
- "it shows the share tab"
- );
-
- assert.ok(
- exists(".share-and-invite.modal .modal-tab.share.single-tab"),
- "it shows only one tab"
- );
-
- assert.ok(
- !exists(".share-and-invite.modal .modal-tab.invite"),
- "it doesn’t show the invite tab"
- );
+ assert.ok(exists("#share-link"), "it shows the share modal");
});
diff --git a/test/javascripts/acceptance/topic-test.js.es6 b/test/javascripts/acceptance/topic-test.js.es6
index cf043848d33..c38e5743452 100644
--- a/test/javascripts/acceptance/topic-test.js.es6
+++ b/test/javascripts/acceptance/topic-test.js.es6
@@ -24,12 +24,9 @@ acceptance("Topic", {
QUnit.test("Share Modal", async assert => {
await visit("/t/internationalization-localization/280");
-
await click(".topic-post:first-child button.share");
- assert.ok(
- exists(".modal.share-and-invite"),
- "it shows the share and invite modal"
- );
+
+ assert.ok(exists("#share-link"), "it shows the share modal");
});
QUnit.test("Showing and hiding the edit controls", async assert => {
diff --git a/test/javascripts/components/share-button-test.js.es6 b/test/javascripts/components/share-button-test.js.es6
new file mode 100644
index 00000000000..35032d2751b
--- /dev/null
+++ b/test/javascripts/components/share-button-test.js.es6
@@ -0,0 +1,16 @@
+import componentTest from "helpers/component-test";
+
+moduleForComponent("share-button", { integration: true });
+
+componentTest("share button", {
+ template: '{{share-button url="https://eviltrout.com"}}',
+
+ test(assert) {
+ assert.ok(find(`button.share`).length, "it has all the classes");
+
+ assert.ok(
+ find('button[data-share-url="https://eviltrout.com"]').length,
+ "it has the data attribute for sharing"
+ );
+ }
+});
diff --git a/test/javascripts/components/share-button.js.es6 b/test/javascripts/components/share-button.js.es6
new file mode 100644
index 00000000000..958f821ce6a
--- /dev/null
+++ b/test/javascripts/components/share-button.js.es6
@@ -0,0 +1,13 @@
+import Button from "discourse/components/d-button";
+
+export default Button.extend({
+ classNames: ["btn-default", "share"],
+ icon: "link",
+ title: "topic.share.help",
+ label: "topic.share.title",
+ attributeBindings: ["url:data-share-url"],
+
+ click() {
+ return true;
+ }
+});