diff --git a/app/assets/javascripts/discourse/app/components/quote-button.js b/app/assets/javascripts/discourse/app/components/quote-button.js
index 2b932be2d67..1672857343e 100644
--- a/app/assets/javascripts/discourse/app/components/quote-button.js
+++ b/app/assets/javascripts/discourse/app/components/quote-button.js
@@ -2,8 +2,16 @@ import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import discourseDebounce from "discourse/lib/debounce";
import toMarkdown from "discourse/lib/to-markdown";
-import { selectedText, selectedElement } from "discourse/lib/utilities";
+import {
+ selectedText,
+ selectedElement,
+ postUrl
+} from "discourse/lib/utilities";
+import { getAbsoluteURL } from "discourse-common/lib/get-url";
import { INPUT_DELAY } from "discourse-common/config/environment";
+import { action } from "@ember/object";
+import discourseComputed from "discourse-common/utils/decorators";
+import Sharing from "discourse/lib/sharing";
function getQuoteTitle(element) {
const titleEl = element.querySelector(".title");
@@ -201,8 +209,59 @@ export default Component.extend({
.off("selectionchange.quote-button");
},
- click() {
+ @discourseComputed
+ quoteSharingEnabled() {
+ if (
+ this.site.mobileView ||
+ this.siteSettings.share_quote_visibility === "none" ||
+ this.quoteSharingSources.length === 0 ||
+ (this.currentUser &&
+ this.siteSettings.share_quote_visibility === "anonymous")
+ ) {
+ return false;
+ }
+
+ return true;
+ },
+
+ @discourseComputed("topic.isPrivateMessage")
+ quoteSharingSources(isPM) {
+ return Sharing.activeSources(
+ this.siteSettings.share_quote_buttons,
+ this.siteSettings.login_required || isPM
+ );
+ },
+
+ @discourseComputed
+ quoteSharingShowLabel() {
+ return this.quoteSharingSources.length > 1;
+ },
+
+ @discourseComputed("topic.{id,slug}", "quoteState")
+ shareUrl(topic, quoteState) {
+ return getAbsoluteURL(postUrl(topic.slug, topic.id, quoteState.postId));
+ },
+
+ @discourseComputed("topic.details.can_create_post", "composer.visible")
+ embedQuoteButton(canCreatePost, composerOpened) {
+ return (
+ (canCreatePost || composerOpened) &&
+ this.currentUser &&
+ this.currentUser.get("enable_quoting")
+ );
+ },
+
+ @action
+ insertQuote() {
this.attrs.selectText().then(() => this._hideButton());
- return false;
+ },
+
+ @action
+ share(source) {
+ Sharing.shareSource(source, {
+ url: this.shareUrl,
+ title: this.topic.title,
+ quote: window.getSelection().toString()
+ });
}
});
diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js
index eba29fa2af2..54e69b2770c 100644
--- a/app/assets/javascripts/discourse/app/controllers/topic.js
+++ b/app/assets/javascripts/discourse/app/controllers/topic.js
@@ -77,15 +77,6 @@ export default Controller.extend(bufferedProperty("model"), {
}
},
- @discourseComputed("model.details.can_create_post", "composer.visible")
- embedQuoteButton(canCreatePost, composerOpened) {
- return (
- (canCreatePost || composerOpened) &&
- this.currentUser &&
- this.currentUser.get("enable_quoting")
- );
- },
-
@discourseComputed("model.postStream.loaded", "model.category_id")
showSharedDraftControls(loaded, categoryId) {
let draftCat = this.site.shared_drafts_category_id;
diff --git a/app/assets/javascripts/discourse/app/initializers/sharing-sources.js b/app/assets/javascripts/discourse/app/initializers/sharing-sources.js
index 127a6e55782..bb0fd892356 100644
--- a/app/assets/javascripts/discourse/app/initializers/sharing-sources.js
+++ b/app/assets/javascripts/discourse/app/initializers/sharing-sources.js
@@ -4,17 +4,17 @@ import Sharing from "discourse/lib/sharing";
export default {
name: "sharing-sources",
- initialize: function() {
+ initialize: function(container) {
+ const siteSettings = container.lookup("site-settings:main");
+
Sharing.addSource({
id: "twitter",
icon: "fab-twitter-square",
- generateUrl: function(link, title) {
- return (
- "http://twitter.com/intent/tweet?url=" +
- encodeURIComponent(link) +
- "&text=" +
- encodeURIComponent(title)
- );
+ generateUrl: function(link, title, quote = "") {
+ const text = quote ? `"${quote}" -- ` : title;
+ return `http://twitter.com/intent/tweet?url=${encodeURIComponent(
+ link
+ )}&text=${encodeURIComponent(text)}`;
},
shouldOpenInPopup: true,
title: I18n.t("share.twitter"),
@@ -25,13 +25,14 @@ export default {
id: "facebook",
icon: "fab-facebook",
title: I18n.t("share.facebook"),
- generateUrl: function(link, title) {
- return (
- "http://www.facebook.com/sharer.php?u=" +
- encodeURIComponent(link) +
- "&t=" +
- encodeURIComponent(title)
- );
+ generateUrl: function(link, title, quote = "") {
+ const fb_url = siteSettings.facebook_app_id
+ ? `https://www.facebook.com/dialog/share?app_id=${
+ siteSettings.facebook_app_id
+ }"e=${encodeURIComponent(quote)}&href=`
+ : "https://www.facebook.com/sharer.php?u=";
+
+ return `${fb_url}${encodeURIComponent(link)}`;
},
shouldOpenInPopup: true
});
@@ -40,17 +41,18 @@ export default {
id: "email",
icon: "envelope-square",
title: I18n.t("share.email"),
- showInPrivateContext: true,
- generateUrl: function(link, title) {
+ generateUrl: function(link, title, quote = "") {
+ const body = quote ? `${quote} \n\n ${link}` : link;
return (
"mailto:?to=&subject=" +
encodeURIComponent(
"[" + Discourse.SiteSettings.title + "] " + title
) +
"&body=" +
- encodeURIComponent(link)
+ encodeURIComponent(body)
);
- }
+ },
+ showInPrivateContext: true
});
}
};
diff --git a/app/assets/javascripts/discourse/app/lib/sharing.js b/app/assets/javascripts/discourse/app/lib/sharing.js
index aa7b0a516b8..5c0dfe5da1e 100644
--- a/app/assets/javascripts/discourse/app/lib/sharing.js
+++ b/app/assets/javascripts/discourse/app/lib/sharing.js
@@ -59,7 +59,7 @@ export default {
if (source.clickHandler) {
source.clickHandler(data.url, data.title);
} else {
- const url = source.generateUrl(data.url, data.title);
+ const url = source.generateUrl(data.url, data.title, data.quote);
const options = {
menubar: "no",
toolbar: "no",
@@ -74,6 +74,8 @@ export default {
if (source.shouldOpenInPopup) {
window.open(url, "", stringOptions);
+ } else if (source.id === "email") {
+ window.location.href = url;
} else {
window.open(url, "_blank");
}
diff --git a/app/assets/javascripts/discourse/app/templates/components/quote-button.hbs b/app/assets/javascripts/discourse/app/templates/components/quote-button.hbs
index 868baa320f5..a28dd8e26ed 100644
--- a/app/assets/javascripts/discourse/app/templates/components/quote-button.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/quote-button.hbs
@@ -1 +1,31 @@
-{{d-icon "quote-left"}} {{i18n "post.quote_reply"}}
+{{#if embedQuoteButton}}
+ {{d-button
+ class="btn-flat insert-quote"
+ action=(action "insertQuote")
+ icon="quote-left"
+ label="post.quote_reply"}}
+{{/if}}
+
+{{#if quoteSharingEnabled}}
+
+ {{#if quoteSharingShowLabel}}
+ {{d-button
+ icon="share"
+ label="post.quote_share"
+ class="btn-flat quote-share-label"}}
+ {{/if}}
+
+
+ {{#each quoteSharingSources as |source|}}
+ {{d-button
+ class="btn-flat"
+ action=(action "share" source)
+ translatedTitle=source.title
+ icon=source.icon}}
+ {{/each}}
+ {{plugin-outlet name="quote-share-buttons-after" tagName=""}}
+
+
+{{/if}}
+
+{{plugin-outlet name="quote-button-after" tagName=""}}
diff --git a/app/assets/javascripts/discourse/app/templates/topic.hbs b/app/assets/javascripts/discourse/app/templates/topic.hbs
index 33f11793a80..f2cddfa09c1 100644
--- a/app/assets/javascripts/discourse/app/templates/topic.hbs
+++ b/app/assets/javascripts/discourse/app/templates/topic.hbs
@@ -397,7 +397,5 @@
{{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}}
- {{#if embedQuoteButton}}
- {{quote-button quoteState=quoteState selectText=(action "selectText")}}
- {{/if}}
+ {{quote-button quoteState=quoteState selectText=(action "selectText") topic=model composerVisible=composer.visible}}
{{/discourse-topic}}
diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss
index 3b2424c2ac8..34746036722 100644
--- a/app/assets/stylesheets/common/base/topic-post.scss
+++ b/app/assets/stylesheets/common/base/topic-post.scss
@@ -1,3 +1,5 @@
+$quote-share-maxwidth: 150px;
+
.button-count.has-pending {
span {
background-color: $danger;
@@ -299,19 +301,65 @@ blockquote {
.quote-button {
display: none;
position: absolute;
- background-color: blend-primary-secondary(50%);
- color: dark-light-choose($secondary, $primary);
- padding: 10px;
z-index: z("dropdown");
opacity: 0.9;
+ background-color: dark-light-choose(
+ blend-primary-secondary(60%),
+ blend-primary-secondary(30%)
+ );
- .d-icon {
- display: inline-block;
+ &.visible {
+ display: block;
}
- &:hover {
- background-color: $primary-medium;
- cursor: pointer;
+ .btn,
+ .btn:hover,
+ .d-icon,
+ .btn:hover .d-icon {
+ color: dark-light-choose($secondary, $primary);
+ }
+
+ .insert-quote + .quote-sharing {
+ border-left: 1px solid
+ dark-light-choose(rgba($secondary, 0.4), rgba($primary, 0.4));
+ }
+
+ .quote-sharing {
+ vertical-align: middle;
+ display: inline-flex;
+ align-items: center;
+
+ .btn {
+ display: inline-flex;
+ align-items: center;
+ }
+
+ .quote-share-label {
+ opacity: 1;
+ max-width: $quote-share-maxwidth;
+ transition: opacity 0.3s ease-in-out, max-width 0.3s ease-in-out,
+ padding 0.3s ease-in-out;
+ }
+
+ &:hover .quote-share-label {
+ background: transparent;
+ opacity: 0;
+ max-width: 0px;
+ padding: 6px 0px;
+ }
+
+ .quote-share-label + .quote-share-buttons {
+ opacity: 0;
+ overflow: hidden;
+ max-width: 0;
+ display: inline-flex;
+ transition: opacity 0.3s ease-in-out, max-width 0.3s ease-in-out;
+ }
+
+ &:hover .quote-share-label + .quote-share-buttons {
+ max-width: $quote-share-maxwidth;
+ opacity: 1;
+ }
}
}
diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss
index ed1b906a302..4a06c3b9bd5 100644
--- a/app/assets/stylesheets/desktop/topic-post.scss
+++ b/app/assets/stylesheets/desktop/topic-post.scss
@@ -465,10 +465,6 @@ button.expand-post {
margin-left: $topic-body-width-padding;
}
-.quote-button.visible {
- display: block;
-}
-
iframe {
max-width: 100%;
}
diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss
index 7031810bc01..4a3ca7d968d 100644
--- a/app/assets/stylesheets/mobile/topic-post.scss
+++ b/app/assets/stylesheets/mobile/topic-post.scss
@@ -279,7 +279,6 @@ span.post-count {
}
.quote-button.visible {
- display: block;
z-index: z("tooltip");
}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index d4eee0c8c74..404a8c14811 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -151,9 +151,9 @@ en:
topic_html: 'Topic: %{topicTitle}'
post: "post #%{postNumber}"
close: "close"
- twitter: "Share this link on Twitter"
- facebook: "Share this link on Facebook"
- email: "Send this link in an email"
+ twitter: "Share on Twitter"
+ facebook: "Share on Facebook"
+ email: "Send via email"
action_codes:
public_topic: "made this topic public %{when}"
@@ -2597,6 +2597,7 @@ en:
post:
quote_reply: "Quote"
+ quote_share: "Share"
edit_reason: "Reason: "
post_number: "post %{number}"
ignored: "Ignored content"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 102879cb257..bc281ef10fe 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -208,6 +208,7 @@ en:
s3_backup_requires_s3_settings: "You cannot use S3 as backup location unless you've provided the '%{setting_name}'."
s3_bucket_reused: "You cannot use the same bucket for 's3_upload_bucket' and 's3_backup_bucket'. Choose a different bucket or use a different path for each bucket."
secure_media_requirements: "S3 uploads must be enabled before enabling secure media."
+ share_quote_facebook_requirements: "You must set a Facebook app id to enable quote sharing for Facebook."
second_factor_cannot_be_enforced_with_disabled_local_login: "You cannot enforce 2FA if local logins are disabled."
local_login_cannot_be_disabled_if_second_factor_enforced: "You cannot disable local login if 2FA is enforced. Disable enforced 2FA before disabling local logins."
cannot_enable_s3_uploads_when_s3_enabled_globally: "You cannot enable S3 uploads because S3 uploads are already globally enabled, and enabling this site-level could cause critical issues with uploads"
@@ -1666,7 +1667,7 @@ en:
instagram_consumer_secret: "Consumer secret Instagram authentication"
enable_facebook_logins: "Enable Facebook authentication, requires facebook_app_id and facebook_app_secret. See Configuring Facebook login for Discourse."
- facebook_app_id: "App id for Facebook authentication, registered at https://developers.facebook.com/apps"
+ facebook_app_id: "App id for Facebook authentication and sharing, registered at https://developers.facebook.com/apps"
facebook_app_secret: "App secret for Facebook authentication, registered at https://developers.facebook.com/apps"
enable_github_logins: "Enable Github authentication, requires github_client_id and github_client_secret. See Configuring GitHub login for Discourse."
@@ -2233,6 +2234,9 @@ en:
gravatar_base_url: "Url of the Gravatar provider's API base"
gravatar_login_url: "Url relative to gravatar_base_url, which provides the user with the login to the Gravatar service"
+ share_quote_buttons: "Determine which items appear in the quote sharing widget, and in what order."
+ share_quote_visibility: "Determine when to show quote sharing buttons: never, to anonymous users only or all users. "
+
errors:
invalid_email: "Invalid email address."
invalid_username: "There's no user with that username."
diff --git a/config/site_settings.yml b/config/site_settings.yml
index aeab2e006ee..03c0d951f09 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -197,6 +197,23 @@ basic:
- twitter
- facebook
- email
+ share_quote_visibility:
+ client: true
+ type: enum
+ default: "anonymous"
+ choices:
+ - none
+ - anonymous
+ - all
+ share_quote_buttons:
+ client: true
+ type: list
+ default: "twitter|email"
+ allow_any: false
+ choices:
+ - twitter
+ - facebook
+ - email
desktop_category_page_style:
client: true
enum: "CategoryPageStyle"
@@ -354,6 +371,7 @@ login:
enable_facebook_logins:
default: false
facebook_app_id:
+ client: true
default: ""
regex: "^\\d+$"
facebook_app_secret:
@@ -1748,7 +1766,7 @@ backups:
search:
search_ranking_normalization:
- default: '0'
+ default: "0"
hidden: true
min_search_term_length:
client: true
diff --git a/lib/site_settings/validations.rb b/lib/site_settings/validations.rb
index 0bf13f7606e..4160a175d25 100644
--- a/lib/site_settings/validations.rb
+++ b/lib/site_settings/validations.rb
@@ -127,6 +127,10 @@ module SiteSettings::Validations
validate_error :secure_media_requirements if new_val == "t" && !SiteSetting.Upload.enable_s3_uploads
end
+ def validate_share_quote_buttons(new_val)
+ validate_error :share_quote_facebook_requirements if new_val.include?("facebook") && SiteSetting.facebook_app_id.blank?
+ end
+
def validate_enable_s3_inventory(new_val)
validate_error :enable_s3_uploads_is_required if new_val == "t" && !SiteSetting.Upload.enable_s3_uploads
end
diff --git a/test/javascripts/acceptance/topic-quote-button-test.js b/test/javascripts/acceptance/topic-quote-button-test.js
new file mode 100644
index 00000000000..181ec543135
--- /dev/null
+++ b/test/javascripts/acceptance/topic-quote-button-test.js
@@ -0,0 +1,122 @@
+import I18n from "I18n";
+import { acceptance } from "helpers/qunit-helpers";
+
+function selectText(selector) {
+ const range = document.createRange();
+ const node = document.querySelector(selector);
+ range.selectNodeContents(node);
+
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+acceptance("Topic - Quote button - logged in", {
+ loggedIn: true,
+ settings: {
+ share_quote_visibility: "anonymous",
+ share_quote_buttons: "twitter|email"
+ }
+});
+
+QUnit.test("Does not show the quote share buttons by default", async assert => {
+ await visit("/t/internationalization-localization/280");
+ selectText("#post_5 blockquote");
+ assert.ok(exists(".insert-quote"), "it shows the quote button");
+ assert.equal(
+ find(".quote-sharing").length,
+ 0,
+ "it does not show quote sharing"
+ );
+});
+
+QUnit.test(
+ "Shows quote share buttons with the right site settings",
+ async function(assert) {
+ this.siteSettings.share_quote_visibility = "all";
+
+ await visit("/t/internationalization-localization/280");
+ selectText("#post_5 blockquote");
+
+ assert.ok(exists(".quote-sharing"), "it shows the quote sharing options");
+ assert.ok(
+ exists(`.quote-sharing .btn[title='${I18n.t("share.twitter")}']`),
+ "it includes the twitter share button"
+ );
+ assert.ok(
+ exists(`.quote-sharing .btn[title='${I18n.t("share.email")}']`),
+ "it includes the email share button"
+ );
+ }
+);
+
+acceptance("Topic - Quote button - anonymous", {
+ loggedIn: false,
+ settings: {
+ share_quote_visibility: "anonymous",
+ share_quote_buttons: "twitter|email"
+ }
+});
+
+QUnit.test(
+ "Shows quote share buttons with the right site settings",
+ async function(assert) {
+ await visit("/t/internationalization-localization/280");
+ selectText("#post_5 blockquote");
+
+ assert.ok(find(".quote-sharing"), "it shows the quote sharing options");
+ assert.ok(
+ exists(`.quote-sharing .btn[title='${I18n.t("share.twitter")}']`),
+ "it includes the twitter share button"
+ );
+ assert.ok(
+ exists(`.quote-sharing .btn[title='${I18n.t("share.email")}']`),
+ "it includes the email share button"
+ );
+ assert.equal(
+ find(".insert-quote").length,
+ 0,
+ "it does not show the quote button"
+ );
+ }
+);
+
+QUnit.test(
+ "Shows single share button when site setting only has one item",
+ async function(assert) {
+ this.siteSettings.share_quote_buttons = "twitter";
+
+ await visit("/t/internationalization-localization/280");
+ selectText("#post_5 blockquote");
+
+ assert.ok(exists(".quote-sharing"), "it shows the quote sharing options");
+ assert.ok(
+ exists(`.quote-sharing .btn[title='${I18n.t("share.twitter")}']`),
+ "it includes the twitter share button"
+ );
+ assert.equal(
+ find(".quote-share-label").length,
+ 0,
+ "it does not show the Share label"
+ );
+ }
+);
+
+QUnit.test("Shows nothing when visibility is disabled", async function(assert) {
+ this.siteSettings.share_quote_visibility = "none";
+
+ await visit("/t/internationalization-localization/280");
+ selectText("#post_5 blockquote");
+
+ assert.equal(
+ find(".quote-sharing").length,
+ 0,
+ "it does not show quote sharing"
+ );
+
+ assert.equal(
+ find(".insert-quote").length,
+ 0,
+ "it does not show the quote button"
+ );
+});
diff --git a/test/javascripts/acceptance/topic-test.js b/test/javascripts/acceptance/topic-test.js
index f795e56f8fc..4e4cb0866fa 100644
--- a/test/javascripts/acceptance/topic-test.js
+++ b/test/javascripts/acceptance/topic-test.js
@@ -341,7 +341,7 @@ function selectText(selector) {
QUnit.test("Quoting a quote keeps the original poster name", async assert => {
await visit("/t/internationalization-localization/280");
selectText("#post_5 blockquote");
- await click(".quote-button");
+ await click(".quote-button .insert-quote");
assert.ok(
find(".d-editor-input")
@@ -386,7 +386,7 @@ QUnit.test(
async assert => {
await visit("/t/internationalization-localization/280");
selectText("#post_5 .cooked");
- await click(".quote-button");
+ await click(".quote-button .insert-quote");
assert.ok(
find(".d-editor-input")