Merge pull request #3760 from riking/signup-cta

Signup cta
This commit is contained in:
Robin Ward 2015-09-16 14:33:25 -04:00
commit 62c20ff5f2
12 changed files with 271 additions and 24 deletions

View File

@ -7,7 +7,7 @@ app/assets/javascripts/vendor.js
app/assets/javascripts/locales/i18n.js
app/assets/javascripts/defer/html-sanitizer-bundle.js
app/assets/javascripts/discourse/lib/Markdown.Editor.js
app/assets/javascripts/ember-addons
app/assets/javascripts/ember-addons/
jsapp/lib/Markdown.Editor.js
lib/javascripts/locale/
lib/javascripts/messageformat.js

View File

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

View File

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

View File

@ -46,6 +46,14 @@ KeyValueStore.prototype = {
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) {
if (!safeLocalStorage) { return null; }
try {

View File

@ -3,7 +3,8 @@
import Singleton from 'discourse/mixins/singleton';
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({
@ -85,9 +86,6 @@ const ScreenTrack = Ember.Object.extend({
flush() {
if (this.get('cancelled')) { return; }
// We don't log anything unless we're logged in
if (!Discourse.User.current()) return;
const newTimings = {},
totalTimings = this.get('totalTimings'),
self = this;
@ -118,26 +116,54 @@ const ScreenTrack = Ember.Object.extend({
this.topicTrackingState.updateSeen(topicId, highestSeen);
if (!$.isEmptyObject(newTimings)) {
Discourse.ajax('/topics/timings', {
data: {
timings: newTimings,
topic_time: this.get('topicTime'),
topic_id: topicId
},
cache: false,
type: 'POST',
headers: {
'X-SILENCE-LOGGER': 'true'
if (Discourse.User.current()) {
Discourse.ajax('/topics/timings', {
data: {
timings: newTimings,
topic_time: this.get('topicTime'),
topic_id: topicId
},
cache: false,
type: 'POST',
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(){
const controller = self.get('topicController');
if(controller){
const postNumbers = Object.keys(newTimings).map(function(v){
return parseInt(v,10);
});
controller.readPosts(topicId, postNumbers);
if (topicIds.indexOf(topicId) === -1 && topicIds.length < ANON_MAX_TOPIC_IDS) {
topicIds.push(topicId);
storage.setItem('anon-topic-ids', topicIds.join(','));
}
});
// Inform the observer
if (this.get('anonFlushCallback')) {
this.get('anonFlushCallback')();
}
// No need to call controller.readPosts()
}
this.set('topicTime', 0);
}

View File

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

View File

@ -86,7 +86,12 @@
{{#if loadedAllPosts}}
{{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}}
<div class="has-pending-posts">

View File

@ -964,6 +964,14 @@ span.highlighted {
transition: opacity ease-out 1s;
}
.signup-cta {
margin-top: 15px;
width: $topic-body-width;
a {
float: right;
text-decoration: underline;
}
}
/* Tablet (portrait) ----------- */

View File

@ -500,6 +500,21 @@ span.highlighted {
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 {
p {
padding-top: 0;

View File

@ -722,6 +722,22 @@ en:
one: reply
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:
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."

View File

@ -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)"
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_google_oauth2_logins: "Enable Google Oauth2 authentication. This is the method of authentication that Google currently supports. Requires key and secret."

View File

@ -202,6 +202,9 @@ login:
allow_new_registrations:
client: true
default: true
enable_signup_cta:
client: true
default: true
enable_google_oauth2_logins:
client: true
default: false