Guild A/B test and Achievement (#8740)
* WIP(guilds): AB test pester modal * WIP(AB-test): guild pester cont'd * fix(style): linting error * fix(AB-test): markModified and notif enum * fix(tests): update AB expectations * fix(modal): remove extra includes * feat(achievements): add Joined Guild cheevo Also removes unused achievement sprites, and properly saves counter used in A/B testing * fix(style): linting error from conflict
@@ -74,6 +74,18 @@ describe('POST /group', () => {
|
||||
expect(updatedUser.guilds).to.include(guild._id);
|
||||
});
|
||||
|
||||
it('awards the Joined Guild achievement', async () => {
|
||||
await user.post('/groups', {
|
||||
name: 'some guild',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
});
|
||||
|
||||
let updatedUser = await user.get('/user');
|
||||
|
||||
expect(updatedUser.achievements.joinedGuild).to.eql(true);
|
||||
});
|
||||
|
||||
context('public guild', () => {
|
||||
it('creates a group', async () => {
|
||||
let groupName = 'Test Public Guild';
|
||||
|
||||
@@ -68,6 +68,12 @@ describe('POST /group/:groupId/join', () => {
|
||||
|
||||
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1);
|
||||
});
|
||||
|
||||
it('awards Joined Guild achievement', async () => {
|
||||
await joiningUser.post(`/groups/${publicGuild._id}/join`);
|
||||
|
||||
await expect(joiningUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
|
||||
});
|
||||
});
|
||||
|
||||
context('Joining a private guild', () => {
|
||||
@@ -147,8 +153,14 @@ describe('POST /group/:groupId/join', () => {
|
||||
}),
|
||||
};
|
||||
|
||||
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[0].data).to.eql(expectedData);
|
||||
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[1].data).to.eql(expectedData);
|
||||
});
|
||||
|
||||
it('awards Joined Guild achievement', async () => {
|
||||
await invitedUser.post(`/groups/${guild._id}/join`);
|
||||
|
||||
await expect(invitedUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,8 +112,8 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
|
||||
@@ -122,7 +122,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(member2.notifications.length).to.equal(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -52,14 +52,14 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
}, 'cs')); // This test only works if we have the notification translated
|
||||
expect(user.notifications[0].data.groupId).to.equal(guild._id);
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(updatedTask.group.approval.requested).to.equal(true);
|
||||
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
@@ -82,14 +82,14 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
}));
|
||||
expect(user.notifications[0].data.groupId).to.equal(guild._id);
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
|
||||
@@ -251,7 +251,6 @@ describe('POST /user/auth/local/register', () => {
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('POST /user/auth/social', () => {
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('POST /user/auth/social', () => {
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 720 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 755 B |
BIN
website/assets/sprites/spritesmith_large/scenes/scene_guilds.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -78,7 +78,7 @@ habitrpg.controller('NotificationCtrl',
|
||||
// Avoid showing the same notiication more than once
|
||||
var lastShownNotifications = [];
|
||||
|
||||
function trasnferGroupNotification(notification) {
|
||||
function transferGroupNotification(notification) {
|
||||
if (!User.user.groupNotifications) User.user.groupNotifications = [];
|
||||
User.user.groupNotifications.push(notification);
|
||||
}
|
||||
@@ -110,6 +110,13 @@ habitrpg.controller('NotificationCtrl',
|
||||
var markAsRead = true;
|
||||
|
||||
switch (notification.type) {
|
||||
case 'GUILD_PROMPT':
|
||||
if (notification.data.textVariant === -1) {
|
||||
$rootScope.openModal('testing');
|
||||
} else {
|
||||
$rootScope.openModal('testingVariant');
|
||||
}
|
||||
break;
|
||||
case 'DROPS_ENABLED':
|
||||
$rootScope.openModal('dropsEnabled');
|
||||
break;
|
||||
@@ -129,12 +136,19 @@ habitrpg.controller('NotificationCtrl',
|
||||
}
|
||||
break;
|
||||
case 'ULTIMATE_GEAR_ACHIEVEMENT':
|
||||
$rootScope.playSound('Achievement_Unlocked');
|
||||
Achievement.displayAchievement('ultimateGear', {size: 'md'});
|
||||
break;
|
||||
case 'REBIRTH_ACHIEVEMENT':
|
||||
$rootScope.playSound('Achievement_Unlocked');
|
||||
Achievement.displayAchievement('rebirth');
|
||||
break;
|
||||
case 'GUILD_JOINED_ACHIEVEMENT':
|
||||
$rootScope.playSound('Achievement_Unlocked');
|
||||
Achievement.displayAchievement('joinedGuild', {size: 'md'});
|
||||
break;
|
||||
case 'NEW_CONTRIBUTOR_LEVEL':
|
||||
$rootScope.playSound('Achievement_Unlocked');
|
||||
Achievement.displayAchievement('contributor', {size: 'md'});
|
||||
break;
|
||||
case 'CRON':
|
||||
@@ -144,11 +158,11 @@ habitrpg.controller('NotificationCtrl',
|
||||
}
|
||||
break;
|
||||
case 'GROUP_TASK_APPROVAL':
|
||||
trasnferGroupNotification(notification);
|
||||
transferGroupNotification(notification);
|
||||
markAsRead = false;
|
||||
break;
|
||||
case 'GROUP_TASK_APPROVED':
|
||||
trasnferGroupNotification(notification);
|
||||
transferGroupNotification(notification);
|
||||
markAsRead = false;
|
||||
break;
|
||||
case 'SCORED_TASK':
|
||||
|
||||
@@ -282,5 +282,7 @@
|
||||
"userIsNotManager": "User is not manager",
|
||||
"canOnlyApproveTaskOnce": "This task has already been approved.",
|
||||
"leaderMarker": " - Leader",
|
||||
"managerMarker": " - Manager"
|
||||
"managerMarker": " - Manager",
|
||||
"joinedGuild": "Joined a Guild",
|
||||
"joinedGuildText": "Ventured into the social side of Habitica by joining a Guild!"
|
||||
}
|
||||
|
||||
7
website/common/locales/en/testing.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"guildReminderTitle": "Check out Guilds!",
|
||||
"guildReminderText1": "Now that you've mastered Habitica's basics, come check out Guilds, where you can chat with other people who are committed to improving their lives!",
|
||||
"guildReminderText2": "Ready for more? Join a Guild to meet other people who are committed to improving their lives!",
|
||||
"guildReminderCTA": "Take me there!",
|
||||
"guildReminderDismiss": "Not right now."
|
||||
}
|
||||
@@ -102,6 +102,11 @@ let basicAchievs = {
|
||||
titleKey: 'royallyLoyal',
|
||||
textKey: 'royallyLoyalText',
|
||||
},
|
||||
joinedGuild: {
|
||||
icon: 'achievement-guild',
|
||||
titleKey: 'joinedGuild',
|
||||
textKey: 'joinedGuildText',
|
||||
},
|
||||
};
|
||||
Object.assign(achievementsData, basicAchievs);
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ function _getBasicAchievements (user, language) {
|
||||
|
||||
_addSimple(result, user, {path: 'partyUp', language});
|
||||
_addSimple(result, user, {path: 'partyOn', language});
|
||||
_addSimple(result, user, {path: 'joinedGuild', language});
|
||||
_addSimple(result, user, {path: 'royallyLoyal', language});
|
||||
|
||||
_addSimpleWithMasterCount(result, user, {path: 'beastMaster', language});
|
||||
|
||||
@@ -122,6 +122,14 @@ api.createGroup = {
|
||||
|
||||
user.balance--;
|
||||
user.guilds.push(group._id);
|
||||
if (!user.achievements.joinedGuild) {
|
||||
user.achievements.joinedGuild = true;
|
||||
user.addNotification('GUILD_JOINED_ACHIEVEMENT');
|
||||
}
|
||||
if (user._ABtests && user._ABtests.guildReminder && user._ABtests.counter !== -1) {
|
||||
user._ABtests.counter = -1;
|
||||
user.markModified('_ABtests');
|
||||
}
|
||||
} else {
|
||||
if (group.privacy !== 'private') throw new NotAuthorized(res.t('partyMustbePrivate'));
|
||||
if (user.party._id) throw new NotAuthorized(res.t('messageGroupAlreadyInParty'));
|
||||
@@ -510,6 +518,14 @@ api.joinGroup = {
|
||||
throw new NotAuthorized(res.t('userAlreadyInGroup'));
|
||||
}
|
||||
user.guilds.push(group._id); // Add group to user's guilds
|
||||
if (!user.achievements.joinedGuild) {
|
||||
user.achievements.joinedGuild = true;
|
||||
user.addNotification('GUILD_JOINED_ACHIEVEMENT');
|
||||
}
|
||||
if (user._ABtests && user._ABtests.guildReminder && user._ABtests.counter !== -1) {
|
||||
user._ABtests.counter = -1;
|
||||
user.markModified('_ABtests');
|
||||
}
|
||||
}
|
||||
if (!isUserInvited) throw new NotAuthorized(res.t('messageGroupRequiresInvite'));
|
||||
|
||||
|
||||
@@ -589,6 +589,18 @@ api.scoreTask = {
|
||||
}
|
||||
}
|
||||
|
||||
if (user._ABtests && user._ABtests.guildReminder && user._ABtests.counter !== -1) {
|
||||
user._ABtests.counter++;
|
||||
if (user._ABtests.counter > 1) {
|
||||
if (user._ABtests.guildReminder.indexOf('timing1') !== -1 || user._ABtests.counter > 4) {
|
||||
user._ABtests.counter = -1;
|
||||
let textVariant = user._ABtests.guildReminder.indexOf('text2');
|
||||
user.addNotification('GUILD_PROMPT', {textVariant});
|
||||
}
|
||||
}
|
||||
user.markModified('_ABtests');
|
||||
}
|
||||
|
||||
if (task.type === 'daily') {
|
||||
task.isDue = common.shouldDo(Date.now(), task, user.preferences);
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ api.getBuyList = {
|
||||
};
|
||||
|
||||
let updatablePaths = [
|
||||
'_ABtests.counter',
|
||||
|
||||
'flags.customizationsNotification',
|
||||
'flags.showTour',
|
||||
'flags.tour',
|
||||
|
||||
@@ -10,7 +10,7 @@ import schema from './schema';
|
||||
schema.plugin(baseModel, {
|
||||
// noSet is not used as updating uses a whitelist and creating only accepts specific params (password, email, username, ...)
|
||||
noSet: [],
|
||||
private: ['auth.local.hashed_password', 'auth.local.passwordHashMethod', 'auth.local.salt', '_cronSignature', '_ABtest', '_ABtests'],
|
||||
private: ['auth.local.hashed_password', 'auth.local.passwordHashMethod', 'auth.local.salt', '_cronSignature', '_ABtests'],
|
||||
toJSONTransform: function userToJSON (plainObj, originalDoc) {
|
||||
plainObj._tmp = originalDoc._tmp; // be sure to send down drop notifs
|
||||
delete plainObj.filters;
|
||||
@@ -94,13 +94,25 @@ function _setUpNewUser (user) {
|
||||
let taskTypes;
|
||||
let iterableFlags = user.flags.toObject();
|
||||
|
||||
user._ABtest = '';
|
||||
// A/B test 2016-12-21: Should we deliver notifications for upcoming incentives on days when users don't receive rewards?
|
||||
if (Math.random() < 0.5) {
|
||||
user._ABtests.checkInModals = '20161221_noCheckInPreviews'; // no 'preview' check-in modals
|
||||
// A/B test 2017-05-11: Can we encourage people to join Guilds with a pester modal?
|
||||
let testGroup = Math.random();
|
||||
if (testGroup < 0.1) {
|
||||
user._ABtests.guildReminder = '20170511_noGuildReminder'; // control group, don't pester about Guilds
|
||||
user._ABtests.counter = -1;
|
||||
} else if (testGroup < 0.235) {
|
||||
user._ABtests.guildReminder = '20170511_text1timing1'; // first sample text, show after two clicks
|
||||
user._ABtests.counter = 0;
|
||||
} else if (testGroup < 0.46) {
|
||||
user._ABtests.guildReminder = '20170511_text2timing1'; // second sample text, show after two clicks
|
||||
user._ABtests.counter = 0;
|
||||
} else if (testGroup < 0.685) {
|
||||
user._ABtests.guildReminder = '20170511_text1timing2'; // first sample text, show after five clicks
|
||||
user._ABtests.counter = 0;
|
||||
} else {
|
||||
user._ABtests.checkInModals = '20161221_showCheckInPreviews'; // show 'preview' check-in modals
|
||||
user._ABtests.guildReminder = '20170511_text2timing2'; // second sample text, show after five clicks
|
||||
user._ABtests.counter = 0;
|
||||
}
|
||||
|
||||
user.items.quests.dustbunnies = 1;
|
||||
user.purchased.background.violet = true;
|
||||
user.preferences.background = 'violet';
|
||||
|
||||
@@ -112,6 +112,7 @@ let schema = new Schema({
|
||||
partyUp: Boolean,
|
||||
partyOn: Boolean,
|
||||
royallyLoyal: Boolean,
|
||||
joinedGuild: Boolean,
|
||||
},
|
||||
|
||||
backer: {
|
||||
@@ -545,7 +546,6 @@ let schema = new Schema({
|
||||
return {};
|
||||
}},
|
||||
pushDevices: [PushDeviceSchema],
|
||||
_ABtest: {type: String}, // deprecated. Superseded by _ABtests
|
||||
_ABtests: {type: Schema.Types.Mixed, default: () => {
|
||||
return {};
|
||||
}},
|
||||
|
||||
@@ -18,6 +18,8 @@ const NOTIFICATION_TYPES = [
|
||||
'GROUP_INVITE_ACCEPTED',
|
||||
'SCORED_TASK',
|
||||
'BOSS_DAMAGE', // Not used currently but kept to avoid validation errors
|
||||
'GUILD_PROMPT',
|
||||
'GUILD_JOINED_ACHIEVEMENT',
|
||||
];
|
||||
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
@@ -117,7 +117,7 @@ script(id='modals/achievements/contributor.html', type='text/ng-template')
|
||||
button.btn.btn-primary(style='margin-top:1em' ng-click='$close()')=env.t('huzzah')
|
||||
+achievementFooter
|
||||
|
||||
//Rebirth
|
||||
// Rebirth
|
||||
script(id='modals/achievements/rebirth.html', type='text/ng-template')
|
||||
.modal-content(style='min-width:28em')
|
||||
.modal-body.text-center
|
||||
@@ -152,3 +152,14 @@ script(id='modals/achievements/partyOn.html', type='text/ng-template')
|
||||
br
|
||||
button.btn.btn-primary(ng-click='$close()')=env.t('huzzah')
|
||||
+achievementFooter
|
||||
|
||||
// Joined Guild
|
||||
script(id='modals/achievements/joinedGuild.html', type='text/ng-template')
|
||||
.modal-content(style='min-width:28em')
|
||||
.modal-body.text-center
|
||||
h3(style='margin-bottom:0')=env.t('modalAchievement')
|
||||
+achievementAvatar('guild',0)
|
||||
p=env.t('joinedGuildText')
|
||||
br
|
||||
button.btn.btn-primary(ng-click='$close()')=env.t('huzzah')
|
||||
+achievementFooter
|
||||
|
||||
@@ -27,6 +27,7 @@ include ./generic.jade
|
||||
include ./tasks-edit.jade
|
||||
include ./task-notes.jade
|
||||
include ./task-extra-notes.jade
|
||||
include ./testing.jade
|
||||
|
||||
//- Settings
|
||||
script(type='text/ng-template', id='modals/change-day-start.html')
|
||||
|
||||
31
website/views/shared/modals/testing.jade
Normal file
@@ -0,0 +1,31 @@
|
||||
script(type='text/ng-template', id='modals/testing.html')
|
||||
.modal-content(style='min-width:28em')
|
||||
.modal-body.text-center(style='padding-bottom:0')
|
||||
h3=env.t('guildReminderTitle')
|
||||
br
|
||||
.scene_guilds.center-block
|
||||
br
|
||||
h4=env.t('guildReminderText1')
|
||||
.modal-footer(style='padding-top:0')
|
||||
.container-fluid
|
||||
.row
|
||||
.col-xs-6.text-center
|
||||
button.btn-lg.btn-default(ng-click='$close()')=env.t('guildReminderDismiss')
|
||||
.col-xs-6.text-center
|
||||
button.btn-lg.btn-primary(ui-sref='options.social.guilds.public', href='/#/options/groups/guilds/public', ng-click='$close()')=env.t('guildReminderCTA')
|
||||
|
||||
script(type='text/ng-template', id='modals/testingVariant.html')
|
||||
.modal-content(style='min-width:28em')
|
||||
.modal-body.text-center(style='padding-bottom:0')
|
||||
h3=env.t('guildReminderTitle')
|
||||
br
|
||||
.scene_guilds.center-block
|
||||
br
|
||||
h4=env.t('guildReminderText2')
|
||||
.modal-footer(style='padding-top:0')
|
||||
.container-fluid
|
||||
.row
|
||||
.col-xs-6.text-center
|
||||
button.btn-lg.btn-default(ng-click='$close()')=env.t('guildReminderDismiss')
|
||||
.col-xs-6.text-center
|
||||
button.btn-lg.btn-primary(ui-sref='options.social.guilds.public', href='/#/options/groups/guilds/public', ng-click='$close()')=env.t('guildReminderCTA')
|
||||