FEATURE: optional quote sharing buttons (#10254)

This commit is contained in:
Penar Musaraj 2020-07-17 14:44:31 -04:00 committed by GitHub
parent 6e94f28cf0
commit bf22f7080d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 330 additions and 56 deletions

View File

@ -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()
});
}
});

View File

@ -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;

View File

@ -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
}&quote=${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
});
}
};

View File

@ -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");
}

View File

@ -1 +1,31 @@
{{d-icon "quote-left"}} <span class="quote-label">{{i18n "post.quote_reply"}}</span>
{{#if embedQuoteButton}}
{{d-button
class="btn-flat insert-quote"
action=(action "insertQuote")
icon="quote-left"
label="post.quote_reply"}}
{{/if}}
{{#if quoteSharingEnabled}}
<span class="quote-sharing">
{{#if quoteSharingShowLabel}}
{{d-button
icon="share"
label="post.quote_share"
class="btn-flat quote-share-label"}}
{{/if}}
<span class="quote-share-buttons">
{{#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=""}}
</span>
</span>
{{/if}}
{{plugin-outlet name="quote-button-after" tagName=""}}

View File

@ -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}}

View File

@ -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;
}
}
}

View File

@ -465,10 +465,6 @@ button.expand-post {
margin-left: $topic-body-width-padding;
}
.quote-button.visible {
display: block;
}
iframe {
max-width: 100%;
}

View File

@ -279,7 +279,6 @@ span.post-count {
}
.quote-button.visible {
display: block;
z-index: z("tooltip");
}

View File

@ -151,9 +151,9 @@ en:
topic_html: 'Topic: <span class="topic-title">%{topicTitle}</span>'
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"

View File

@ -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 <a href='https://meta.discourse.org/t/13394' target='_blank'>Configuring Facebook login for Discourse</a>."
facebook_app_id: "App id for Facebook authentication, registered at <a href='https://developers.facebook.com/apps/' target='_blank'>https://developers.facebook.com/apps</a>"
facebook_app_id: "App id for Facebook authentication and sharing, registered at <a href='https://developers.facebook.com/apps/' target='_blank'>https://developers.facebook.com/apps</a>"
facebook_app_secret: "App secret for Facebook authentication, registered at <a href='https://developers.facebook.com/apps/' target='_blank'>https://developers.facebook.com/apps</a>"
enable_github_logins: "Enable Github authentication, requires github_client_id and github_client_secret. See <a href='https://meta.discourse.org/t/13745' target='_blank'>Configuring GitHub login for Discourse</a>."
@ -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."

View File

@ -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

View File

@ -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

View File

@ -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"
);
});

View File

@ -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")