mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 02:40:53 -06:00
FEATURE: optional quote sharing buttons (#10254)
This commit is contained in:
parent
6e94f28cf0
commit
bf22f7080d
@ -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()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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=""}}
|
||||
|
@ -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}}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -465,10 +465,6 @@ button.expand-post {
|
||||
margin-left: $topic-body-width-padding;
|
||||
}
|
||||
|
||||
.quote-button.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
@ -279,7 +279,6 @@ span.post-count {
|
||||
}
|
||||
|
||||
.quote-button.visible {
|
||||
display: block;
|
||||
z-index: z("tooltip");
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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."
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
122
test/javascripts/acceptance/topic-quote-button-test.js
Normal file
122
test/javascripts/acceptance/topic-quote-button-test.js
Normal 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"
|
||||
);
|
||||
});
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user