commit
62c20ff5f2
|
@ -7,7 +7,7 @@ app/assets/javascripts/vendor.js
|
||||||
app/assets/javascripts/locales/i18n.js
|
app/assets/javascripts/locales/i18n.js
|
||||||
app/assets/javascripts/defer/html-sanitizer-bundle.js
|
app/assets/javascripts/defer/html-sanitizer-bundle.js
|
||||||
app/assets/javascripts/discourse/lib/Markdown.Editor.js
|
app/assets/javascripts/discourse/lib/Markdown.Editor.js
|
||||||
app/assets/javascripts/ember-addons
|
app/assets/javascripts/ember-addons/
|
||||||
jsapp/lib/Markdown.Editor.js
|
jsapp/lib/Markdown.Editor.js
|
||||||
lib/javascripts/locale/
|
lib/javascripts/locale/
|
||||||
lib/javascripts/messageformat.js
|
lib/javascripts/messageformat.js
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
action: "showCreateAccount",
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
neverShow() {
|
||||||
|
this.keyValueStore.setItem('anon-cta-never', 't');
|
||||||
|
this.session.set('showSignupCta', false);
|
||||||
|
},
|
||||||
|
hideForSession() {
|
||||||
|
this.session.set('hideSignupCta', true);
|
||||||
|
this.keyValueStore.setItem('anon-cta-hidden', new Date().getTime());
|
||||||
|
Em.run.later(() =>
|
||||||
|
this.session.set('showSignupCta', false),
|
||||||
|
20 * 1000);
|
||||||
|
},
|
||||||
|
showCreateAccount() {
|
||||||
|
this.sendAction();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
signupMethodsTranslated: function() {
|
||||||
|
const methods = Ember.get('Discourse.LoginMethod.all');
|
||||||
|
const loginWithEmail = this.siteSettings.enable_local_logins;
|
||||||
|
if (this.siteSettings.enable_sso) {
|
||||||
|
return I18n.t('signup_cta.methods.sso');
|
||||||
|
} else if (methods.length === 0) {
|
||||||
|
if (loginWithEmail) {
|
||||||
|
return I18n.t('signup_cta.methods.only_email');
|
||||||
|
} else {
|
||||||
|
return I18n.t('signup_cta.methods.unknown');
|
||||||
|
}
|
||||||
|
} else if (methods.length === 1) {
|
||||||
|
let providerName = methods[0].name.capitalize();
|
||||||
|
if (providerName === "Google_oauth2") {
|
||||||
|
providerName = "Google";
|
||||||
|
}
|
||||||
|
if (loginWithEmail) {
|
||||||
|
return I18n.t('signup_cta.methods.one_and_email', {provider: providerName});
|
||||||
|
} else {
|
||||||
|
return I18n.t('signup_cta.methods.only_other', {provider: providerName});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (loginWithEmail) {
|
||||||
|
return I18n.t('signup_cta.methods.multiple', {count: methods.length});
|
||||||
|
} else {
|
||||||
|
return I18n.t('signup_cta.methods.multiple_no_email', {count: methods.length});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.property(),
|
||||||
|
|
||||||
|
_turnOffIfHidden: function() {
|
||||||
|
if (this.session.get('hideSignupCta')) {
|
||||||
|
this.session.set('showSignupCta', false);
|
||||||
|
}
|
||||||
|
}.on('willDestroyElement')
|
||||||
|
});
|
|
@ -0,0 +1,90 @@
|
||||||
|
import ScreenTrack from 'discourse/lib/screen-track';
|
||||||
|
import Session from 'discourse/models/session';
|
||||||
|
|
||||||
|
const ANON_TOPIC_IDS = 5,
|
||||||
|
ANON_PROMPT_READ_TIME = 15 * 60 * 1000,
|
||||||
|
ANON_PROMPT_VISIT_COUNT = 2,
|
||||||
|
ONE_DAY = 24 * 60 * 60 * 1000,
|
||||||
|
PROMPT_HIDE_DURATION = ONE_DAY;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "signup-cta",
|
||||||
|
|
||||||
|
initialize(container) {
|
||||||
|
const screenTrack = ScreenTrack.current(),
|
||||||
|
session = Session.current(),
|
||||||
|
siteSettings = container.lookup('site-settings:main'),
|
||||||
|
keyValueStore = container.lookup('key-value-store:main'),
|
||||||
|
user = container.lookup('current-user:main');
|
||||||
|
|
||||||
|
screenTrack.set('keyValueStore', keyValueStore);
|
||||||
|
|
||||||
|
// Preconditions
|
||||||
|
|
||||||
|
if (user) return; // must not be logged in
|
||||||
|
if (keyValueStore.get('anon-cta-never')) return; // "never show again"
|
||||||
|
if (!siteSettings.allow_new_registrations) return;
|
||||||
|
if (siteSettings.invite_only) return;
|
||||||
|
if (siteSettings.must_approve_users) return;
|
||||||
|
if (siteSettings.login_required) return;
|
||||||
|
if (!siteSettings.enable_signup_cta) return;
|
||||||
|
|
||||||
|
function checkSignupCtaRequirements() {
|
||||||
|
if (session.get('showSignupCta')) {
|
||||||
|
return; // already shown
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.get('hideSignupCta')) {
|
||||||
|
return; // hidden for session
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyValueStore.get('anon-cta-never')) {
|
||||||
|
return; // hidden forever
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const hiddenAt = keyValueStore.getInt('anon-cta-hidden', 0);
|
||||||
|
if (hiddenAt > (now - PROMPT_HIDE_DURATION)) {
|
||||||
|
return; // hidden in last 24 hours
|
||||||
|
}
|
||||||
|
|
||||||
|
const visitCount = keyValueStore.getInt('anon-visit-count');
|
||||||
|
if (visitCount < ANON_PROMPT_VISIT_COUNT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readTime = keyValueStore.getInt('anon-topic-time');
|
||||||
|
if (readTime < ANON_PROMPT_READ_TIME) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const topicIdsString = keyValueStore.get('anon-topic-ids');
|
||||||
|
if (!topicIdsString) { return; }
|
||||||
|
let topicIdsAry = topicIdsString.split(',');
|
||||||
|
if (topicIdsAry.length < ANON_TOPIC_IDS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requirements met.
|
||||||
|
session.set('showSignupCta', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
screenTrack.set('anonFlushCallback', checkSignupCtaRequirements);
|
||||||
|
|
||||||
|
// Record a visit
|
||||||
|
const nowVisit = new Date().getTime();
|
||||||
|
const lastVisit = keyValueStore.getInt('anon-last-visit', 0);
|
||||||
|
if (!lastVisit) {
|
||||||
|
// First visit
|
||||||
|
keyValueStore.setItem('anon-visit-count', 1);
|
||||||
|
keyValueStore.setItem('anon-last-visit', nowVisit);
|
||||||
|
} else if (nowVisit - lastVisit > ONE_DAY) {
|
||||||
|
// More than a day
|
||||||
|
const visitCount = keyValueStore.getInt('anon-visit-count', 1);
|
||||||
|
keyValueStore.setItem('anon-visit-count', visitCount + 1);
|
||||||
|
keyValueStore.setItem('anon-last-visit', nowVisit);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSignupCtaRequirements();
|
||||||
|
}
|
||||||
|
};
|
|
@ -46,6 +46,14 @@ KeyValueStore.prototype = {
|
||||||
return safeLocalStorage[this.context + key];
|
return safeLocalStorage[this.context + key];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInt(key, def) {
|
||||||
|
if (!def) { def = 0; }
|
||||||
|
if (!safeLocalStorage) { return def; }
|
||||||
|
const result = parseInt(this.get(key));
|
||||||
|
if (!isFinite(result)) { return def; }
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
getObject(key) {
|
getObject(key) {
|
||||||
if (!safeLocalStorage) { return null; }
|
if (!safeLocalStorage) { return null; }
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
import Singleton from 'discourse/mixins/singleton';
|
import Singleton from 'discourse/mixins/singleton';
|
||||||
|
|
||||||
const PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3,
|
const PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3,
|
||||||
MAX_TRACKING_TIME = 1000 * 60 * 6;
|
MAX_TRACKING_TIME = 1000 * 60 * 6,
|
||||||
|
ANON_MAX_TOPIC_IDS = 5;
|
||||||
|
|
||||||
const ScreenTrack = Ember.Object.extend({
|
const ScreenTrack = Ember.Object.extend({
|
||||||
|
|
||||||
|
@ -85,9 +86,6 @@ const ScreenTrack = Ember.Object.extend({
|
||||||
flush() {
|
flush() {
|
||||||
if (this.get('cancelled')) { return; }
|
if (this.get('cancelled')) { return; }
|
||||||
|
|
||||||
// We don't log anything unless we're logged in
|
|
||||||
if (!Discourse.User.current()) return;
|
|
||||||
|
|
||||||
const newTimings = {},
|
const newTimings = {},
|
||||||
totalTimings = this.get('totalTimings'),
|
totalTimings = this.get('totalTimings'),
|
||||||
self = this;
|
self = this;
|
||||||
|
@ -118,26 +116,54 @@ const ScreenTrack = Ember.Object.extend({
|
||||||
this.topicTrackingState.updateSeen(topicId, highestSeen);
|
this.topicTrackingState.updateSeen(topicId, highestSeen);
|
||||||
|
|
||||||
if (!$.isEmptyObject(newTimings)) {
|
if (!$.isEmptyObject(newTimings)) {
|
||||||
Discourse.ajax('/topics/timings', {
|
if (Discourse.User.current()) {
|
||||||
data: {
|
Discourse.ajax('/topics/timings', {
|
||||||
timings: newTimings,
|
data: {
|
||||||
topic_time: this.get('topicTime'),
|
timings: newTimings,
|
||||||
topic_id: topicId
|
topic_time: this.get('topicTime'),
|
||||||
},
|
topic_id: topicId
|
||||||
cache: false,
|
},
|
||||||
type: 'POST',
|
cache: false,
|
||||||
headers: {
|
type: 'POST',
|
||||||
'X-SILENCE-LOGGER': 'true'
|
headers: {
|
||||||
|
'X-SILENCE-LOGGER': 'true'
|
||||||
|
}
|
||||||
|
}).then(function() {
|
||||||
|
const controller = self.get('topicController');
|
||||||
|
if (controller) {
|
||||||
|
const postNumbers = Object.keys(newTimings).map(function(v) {
|
||||||
|
return parseInt(v, 10);
|
||||||
|
});
|
||||||
|
controller.readPosts(topicId, postNumbers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Anonymous viewer - save to localStorage
|
||||||
|
const storage = this.get('keyValueStore');
|
||||||
|
|
||||||
|
// Save total time
|
||||||
|
const existingTime = storage.getInt('anon-topic-time');
|
||||||
|
storage.setItem('anon-topic-time', existingTime + this.get('topicTime'));
|
||||||
|
|
||||||
|
// Save unique topic IDs up to a max
|
||||||
|
let topicIds = storage.get('anon-topic-ids');
|
||||||
|
if (topicIds) {
|
||||||
|
topicIds = topicIds.split(',').map(e => parseInt(e));
|
||||||
|
} else {
|
||||||
|
topicIds = [];
|
||||||
}
|
}
|
||||||
}).then(function(){
|
if (topicIds.indexOf(topicId) === -1 && topicIds.length < ANON_MAX_TOPIC_IDS) {
|
||||||
const controller = self.get('topicController');
|
topicIds.push(topicId);
|
||||||
if(controller){
|
storage.setItem('anon-topic-ids', topicIds.join(','));
|
||||||
const postNumbers = Object.keys(newTimings).map(function(v){
|
|
||||||
return parseInt(v,10);
|
|
||||||
});
|
|
||||||
controller.readPosts(topicId, postNumbers);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Inform the observer
|
||||||
|
if (this.get('anonFlushCallback')) {
|
||||||
|
this.get('anonFlushCallback')();
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to call controller.readPosts()
|
||||||
|
}
|
||||||
|
|
||||||
this.set('topicTime', 0);
|
this.set('topicTime', 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<div class="signup-cta alert alert-info">
|
||||||
|
{{#if session.hideSignupCta}}
|
||||||
|
<p>
|
||||||
|
{{i18n "signup_cta.hidden_for_session"}}
|
||||||
|
<a {{action "neverShow"}}>{{i18n "signup_cta.hide_forever"}}</a>
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p>{{i18n "signup_cta.intro"}}</p>
|
||||||
|
<p>{{i18n "signup_cta.value_prop"}}</p>
|
||||||
|
<p>{{signupMethodsTranslated}}</p>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
{{d-button action="showCreateAccount" label="signup_cta.sign_up" icon="check" class="btn-primary"}}
|
||||||
|
{{d-button action="hideForSession" label="signup_cta.hide_session" class="no-icon"}}
|
||||||
|
<a {{action "neverShow"}}>{{i18n "signup_cta.hide_forever"}}</a>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
|
@ -86,7 +86,12 @@
|
||||||
{{#if loadedAllPosts}}
|
{{#if loadedAllPosts}}
|
||||||
|
|
||||||
{{view "topic-closing" topic=model}}
|
{{view "topic-closing" topic=model}}
|
||||||
{{view "topic-footer-buttons" topic=model}}
|
{{#if session.showSignupCta}}
|
||||||
|
{{! replace "Log In to Reply" with the infobox }}
|
||||||
|
{{signup-cta}}
|
||||||
|
{{else}}
|
||||||
|
{{view "topic-footer-buttons" topic=model}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if model.pending_posts_count}}
|
{{#if model.pending_posts_count}}
|
||||||
<div class="has-pending-posts">
|
<div class="has-pending-posts">
|
||||||
|
|
|
@ -964,6 +964,14 @@ span.highlighted {
|
||||||
transition: opacity ease-out 1s;
|
transition: opacity ease-out 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signup-cta {
|
||||||
|
margin-top: 15px;
|
||||||
|
width: $topic-body-width;
|
||||||
|
a {
|
||||||
|
float: right;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Tablet (portrait) ----------- */
|
/* Tablet (portrait) ----------- */
|
||||||
|
|
||||||
|
|
|
@ -500,6 +500,21 @@ span.highlighted {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signup-cta {
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: calc(100% - 50px);
|
||||||
|
a {
|
||||||
|
float: right;
|
||||||
|
text-decoration: underline;
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin-right: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.small-action .small-action-desc {
|
.small-action .small-action-desc {
|
||||||
p {
|
p {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|
|
@ -722,6 +722,22 @@ en:
|
||||||
one: reply
|
one: reply
|
||||||
other: replies
|
other: replies
|
||||||
|
|
||||||
|
signup_cta:
|
||||||
|
sign_up: "Sign Up"
|
||||||
|
hide_session: "Remind me tomorrow"
|
||||||
|
hide_forever: "Never show this again"
|
||||||
|
hidden_for_session: "OK, I'll ask you tomorrow. You can always click the 'Log In' button to create an account, too."
|
||||||
|
intro: Hey there! Looks like you're enjoying the forum, but you're not signed up for an account.
|
||||||
|
value_prop: Logged-in users get their last read position in every topic saved, so you come right back where you left off. You can also "Watch" topics so that you get a notification whenever a new post is made, and you can give likes to others' posts.
|
||||||
|
methods:
|
||||||
|
sso: "Use your account on the main site to log in."
|
||||||
|
only_email: "Signing up is easy: you just need a valid email account and a password."
|
||||||
|
only_other: "Use your %{provider} account to sign up."
|
||||||
|
one_and_email: "Use your %{provider} account, or an email and password, to sign up."
|
||||||
|
multiple_no_email: "Choose from any of the %{count} supported login providers to get started."
|
||||||
|
multiple: "Signing up couldn't be easier: use any of the %{count} available login providers, or sign up with an email and password."
|
||||||
|
unknown: "error getting supported login methods"
|
||||||
|
|
||||||
summary:
|
summary:
|
||||||
enabled_description: "You're viewing a summary of this topic: the most interesting posts as determined by the community."
|
enabled_description: "You're viewing a summary of this topic: the most interesting posts as determined by the community."
|
||||||
description: "There are <b>{{count}}</b> replies."
|
description: "There are <b>{{count}}</b> replies."
|
||||||
|
|
|
@ -921,6 +921,7 @@ en:
|
||||||
|
|
||||||
enable_local_logins: "Enable local username and password login based accounts. (Note: this must be enabled for invites to work)"
|
enable_local_logins: "Enable local username and password login based accounts. (Note: this must be enabled for invites to work)"
|
||||||
allow_new_registrations: "Allow new user registrations. Uncheck this to prevent anyone from creating a new account."
|
allow_new_registrations: "Allow new user registrations. Uncheck this to prevent anyone from creating a new account."
|
||||||
|
enable_signup_cta: "Show a notice to returning anonymous users prompting them to sign up for an account."
|
||||||
enable_yahoo_logins: "Enable Yahoo authentication"
|
enable_yahoo_logins: "Enable Yahoo authentication"
|
||||||
|
|
||||||
enable_google_oauth2_logins: "Enable Google Oauth2 authentication. This is the method of authentication that Google currently supports. Requires key and secret."
|
enable_google_oauth2_logins: "Enable Google Oauth2 authentication. This is the method of authentication that Google currently supports. Requires key and secret."
|
||||||
|
|
|
@ -202,6 +202,9 @@ login:
|
||||||
allow_new_registrations:
|
allow_new_registrations:
|
||||||
client: true
|
client: true
|
||||||
default: true
|
default: true
|
||||||
|
enable_signup_cta:
|
||||||
|
client: true
|
||||||
|
default: true
|
||||||
enable_google_oauth2_logins:
|
enable_google_oauth2_logins:
|
||||||
client: true
|
client: true
|
||||||
default: false
|
default: false
|
||||||
|
|
Loading…
Reference in New Issue