mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
Api v3 fixes continued (#7205)
* Added timzeone offset back * Added APIToken back to settings page * Fixed fetch recent messages for party * Fixed returning group description * Fixed check if user is member of challenge * Fixed party members appearing in header * Updated get myGroups param to include public groups. Fixed isMemberOf group * Fixed hourglass purchase * Fixed challenge addding tasks on first creating * Updated tests to accomidate new changes
This commit is contained in:
committed by
Matteo Pagliazzi
parent
cc20812674
commit
1fd7df7521
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
describe('Challenges Controller', function() {
|
describe('Challenges Controller', function() {
|
||||||
var rootScope, scope, user, User, ctrl, groups, members, notification, state, challenges;
|
var rootScope, scope, user, User, ctrl, groups, members, notification, state, challenges, tasks;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
module(function($provide) {
|
module(function($provide) {
|
||||||
@@ -14,7 +14,7 @@ describe('Challenges Controller', function() {
|
|||||||
$provide.value('User', User);
|
$provide.value('User', User);
|
||||||
});
|
});
|
||||||
|
|
||||||
inject(function($rootScope, $controller, _$state_, _Groups_, _Members_, _Notification_, _Challenges_){
|
inject(function($rootScope, $controller, _$state_, _Groups_, _Members_, _Notification_, _Challenges_, _Tasks_){
|
||||||
scope = $rootScope.$new();
|
scope = $rootScope.$new();
|
||||||
rootScope = $rootScope;
|
rootScope = $rootScope;
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ describe('Challenges Controller', function() {
|
|||||||
ctrl = $controller('ChallengesCtrl', {$scope: scope, User: User});
|
ctrl = $controller('ChallengesCtrl', {$scope: scope, User: User});
|
||||||
|
|
||||||
challenges = _Challenges_;
|
challenges = _Challenges_;
|
||||||
|
tasks = _Tasks_;
|
||||||
groups = _Groups_;
|
groups = _Groups_;
|
||||||
members = _Members_;
|
members = _Members_;
|
||||||
notification = _Notification_;
|
notification = _Notification_;
|
||||||
@@ -320,13 +321,17 @@ describe('Challenges Controller', function() {
|
|||||||
|
|
||||||
context('challenge owner interactions', function() {
|
context('challenge owner interactions', function() {
|
||||||
describe("save challenge", function() {
|
describe("save challenge", function() {
|
||||||
var alert, createChallengeSpy, challengeResponse;
|
var alert, createChallengeSpy, challengeResponse, taskChallengeCreateSpy;
|
||||||
|
|
||||||
beforeEach(function(){
|
beforeEach(function(){
|
||||||
alert = sandbox.stub(window, "alert");
|
alert = sandbox.stub(window, "alert");
|
||||||
createChallengeSpy = sinon.stub(challenges, 'createChallenge');
|
createChallengeSpy = sinon.stub(challenges, 'createChallenge');
|
||||||
challengeResponse = {data: {data: {_id: 'new-challenge'}}};
|
challengeResponse = {data: {data: {_id: 'new-challenge'}}};
|
||||||
createChallengeSpy.returns(Promise.resolve(challengeResponse));
|
createChallengeSpy.returns(Promise.resolve(challengeResponse));
|
||||||
|
|
||||||
|
taskChallengeCreateSpy = sinon.stub(tasks, 'createChallengeTasks');
|
||||||
|
var taskResponse = {data: {data: []}};
|
||||||
|
taskChallengeCreateSpy.returns(Promise.resolve(taskResponse));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens an alert box if challenge.group is not specified", function() {
|
it("opens an alert box if challenge.group is not specified", function() {
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ describe('groupServices', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls party endpoint', function() {
|
it('calls party endpoint', function() {
|
||||||
$httpBackend.expectGET(groupApiUrlPrefix + '/party').respond({});
|
var groupId = '1234';
|
||||||
|
var groupResponse = {data: {_id: groupId}};
|
||||||
|
$httpBackend.expectGET(groupApiUrlPrefix + '/party').respond(groupResponse);
|
||||||
|
$httpBackend.expectGET('/api/v3/groups/' + groupId + '/members?includeAllPublicFields=true').respond({});
|
||||||
groups.Group.syncParty();
|
groups.Group.syncParty();
|
||||||
$httpBackend.flush();
|
$httpBackend.flush();
|
||||||
});
|
});
|
||||||
@@ -74,7 +77,10 @@ describe('groupServices', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls party endpoint when party is not cached', function() {
|
it('calls party endpoint when party is not cached', function() {
|
||||||
$httpBackend.expectGET(groupApiUrlPrefix + '/party').respond({});
|
var groupId = '1234';
|
||||||
|
var groupResponse = {data: {_id: groupId}};
|
||||||
|
$httpBackend.expectGET(groupApiUrlPrefix + '/party').respond(groupResponse);
|
||||||
|
$httpBackend.expectGET('/api/v3/groups/' + groupId + '/members?includeAllPublicFields=true').respond({});
|
||||||
groups.party();
|
groups.party();
|
||||||
$httpBackend.flush();
|
$httpBackend.flush();
|
||||||
});
|
});
|
||||||
@@ -136,7 +142,7 @@ describe('groupServices', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls my guilds endpoint', function() {
|
it('calls my guilds endpoint', function() {
|
||||||
$httpBackend.expectGET(groupApiUrlPrefix + '?type=privateGuilds').respond([]);
|
$httpBackend.expectGET(groupApiUrlPrefix + '?type=guilds').respond([]);
|
||||||
groups.myGuilds();
|
groups.myGuilds();
|
||||||
$httpBackend.flush();
|
$httpBackend.flush();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ describe('Quests Service', function() {
|
|||||||
fakeBackend.when('GET', 'partials/main.html').respond({});
|
fakeBackend.when('GET', 'partials/main.html').respond({});
|
||||||
fakeBackend.when('GET', 'partials/main.html').respond({});
|
fakeBackend.when('GET', 'partials/main.html').respond({});
|
||||||
fakeBackend.when('GET', '/api/v3/groups/party').respond(partyResponse);
|
fakeBackend.when('GET', '/api/v3/groups/party').respond(partyResponse);
|
||||||
|
fakeBackend.when('GET', '/api/v3/groups/party-id/members?includeAllPublicFields=true').respond({});
|
||||||
fakeBackend.when('POST', '/api/v3/groups/party-id/quests/invite/' + key).respond({quest: { key: 'whale' } });
|
fakeBackend.when('POST', '/api/v3/groups/party-id/quests/invite/' + key).respond({quest: { key: 'whale' } });
|
||||||
fakeBackend.flush();
|
fakeBackend.flush();
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.isUserMemberOf = function (challenge) {
|
||||||
|
return User.user.challenges.indexOf(challenge._id) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
$scope.editTask = Tasks.editTask;
|
$scope.editTask = Tasks.editTask;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,21 +128,25 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
|
var _challenge;
|
||||||
Challenges.createChallenge(challenge)
|
Challenges.createChallenge(challenge)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
var _challenge = response.data.data;
|
_challenge = response.data.data;
|
||||||
Notification.text(window.env.t('challengeCreated'));
|
Notification.text(window.env.t('challengeCreated'));
|
||||||
User.sync();
|
User.sync();
|
||||||
|
|
||||||
|
var challengeTasks = [];
|
||||||
|
challengeTasks = challengeTasks.concat(challenge.todos);
|
||||||
|
challengeTasks = challengeTasks.concat(challenge.habits);
|
||||||
|
challengeTasks = challengeTasks.concat(challenge.dailys);
|
||||||
|
challengeTasks = challengeTasks.concat(challenge.rewards);
|
||||||
|
|
||||||
|
return Tasks.createChallengeTasks(_challenge._id, challengeTasks);
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
$state.transitionTo('options.social.challenges.detail', { cid: _challenge._id }, {
|
$state.transitionTo('options.social.challenges.detail', { cid: _challenge._id }, {
|
||||||
reload: true, inherit: false, notify: true
|
reload: true, inherit: false, notify: true
|
||||||
});
|
});
|
||||||
|
|
||||||
var challengeTasks = [];
|
|
||||||
challengeTasks.concat(challenge.todos);
|
|
||||||
challengeTasks.concat(challenge.habits);
|
|
||||||
challengeTasks.concat(challenge.dailys);
|
|
||||||
challengeTasks.concat(challenge.reqards);
|
|
||||||
Tasks.createChallengeTasks(_challenge._id, challengeTasks);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Challenges.updateChallenge(challenge._id, challenge)
|
Challenges.updateChallenge(challenge._id, challenge)
|
||||||
@@ -169,6 +177,7 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
|
|||||||
challenge.winner = undefined;
|
challenge.winner = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//@TODO: change to $scope.remove
|
||||||
$scope["delete"] = function(challenge) {
|
$scope["delete"] = function(challenge) {
|
||||||
var warningMsg;
|
var warningMsg;
|
||||||
|
|
||||||
@@ -232,7 +241,8 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
|
|||||||
//------------------------------------------------------------
|
//------------------------------------------------------------
|
||||||
$scope.addTask = function(addTo, listDef, challenge) {
|
$scope.addTask = function(addTo, listDef, challenge) {
|
||||||
var task = Shared.taskDefaults({text: listDef.newTask, type: listDef.type});
|
var task = Shared.taskDefaults({text: listDef.newTask, type: listDef.type});
|
||||||
Tasks.createChallengeTasks(challenge._id, task);
|
//If the challenge has not been created, we bulk add tasks on save
|
||||||
|
if (challenge._id) Tasks.createChallengeTasks(challenge._id, task);
|
||||||
if (!challenge[task.type + 's']) challenge[task.type + 's'] = [];
|
if (!challenge[task.type + 's']) challenge[task.type + 's'] = [];
|
||||||
challenge[task.type + 's'].unshift(task);
|
challenge[task.type + 's'].unshift(task);
|
||||||
delete listDef.newTask;
|
delete listDef.newTask;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ habitrpg.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.likeChatMessage = function(group,message) {
|
$scope.likeChatMessage = function(group, message) {
|
||||||
if (message.uuid == User.user._id)
|
if (message.uuid == User.user._id)
|
||||||
return Notification.text(window.env.t('foreverAlone'));
|
return Notification.text(window.env.t('foreverAlone'));
|
||||||
|
|
||||||
@@ -114,14 +114,13 @@ habitrpg.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'A
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.sync = function(group){
|
$scope.sync = function(group) {
|
||||||
if(group.type == 'party') {
|
//@TODO: We need to use chat service here
|
||||||
group.$syncParty(); // Syncs the whole party, not just 15 members
|
Groups.Group.get(group._id)
|
||||||
} else {
|
.then(function (response) {
|
||||||
group.$get();
|
$scope.group = response.data.data;
|
||||||
}
|
})
|
||||||
// When the user clicks fetch recent messages we need to update
|
|
||||||
// that the user has seen the new messages
|
|
||||||
Chat.markChatSeen(group._id);
|
Chat.markChatSeen(group._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('habitrpg')
|
angular.module('habitrpg')
|
||||||
.factory('Groups', [ '$location', '$rootScope', '$http', 'Analytics', 'ApiUrl', 'Challenges', '$q', 'User',
|
.factory('Groups', [ '$location', '$rootScope', '$http', 'Analytics', 'ApiUrl', 'Challenges', '$q', 'User', 'Members',
|
||||||
function($location, $rootScope, $http, Analytics, ApiUrl, Challenges, $q, User) {
|
function($location, $rootScope, $http, Analytics, ApiUrl, Challenges, $q, User, Members) {
|
||||||
var data = {party: undefined, myGuilds: undefined, publicGuilds: undefined, tavern: undefined };
|
var data = {party: undefined, myGuilds: undefined, publicGuilds: undefined, tavern: undefined };
|
||||||
var groupApiURLPrefix = "/api/v3/groups";
|
var groupApiURLPrefix = "/api/v3/groups";
|
||||||
|
|
||||||
@@ -117,7 +117,11 @@ angular.module('habitrpg')
|
|||||||
Group.get('party')
|
Group.get('party')
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
data.party = response.data.data;
|
data.party = response.data.data;
|
||||||
_cachedPartyPromise.resolve(data.party);
|
Members.getGroupMembers(data.party._id, true)
|
||||||
|
.then(function (response) {
|
||||||
|
data.party.members = response.data.data;
|
||||||
|
_cachedPartyPromise.resolve(data.party);
|
||||||
|
});
|
||||||
}, function (response) {
|
}, function (response) {
|
||||||
data.party = { type: 'party' };
|
data.party = { type: 'party' };
|
||||||
_cachedPartyPromise.reject(data.party);
|
_cachedPartyPromise.reject(data.party);
|
||||||
@@ -154,7 +158,7 @@ angular.module('habitrpg')
|
|||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
if (!data.myGuilds) {
|
if (!data.myGuilds) {
|
||||||
Group.getGroups('privateGuilds')
|
Group.getGroups('guilds')
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
data.myGuilds = response.data.data;
|
data.myGuilds = response.data.data;
|
||||||
deferred.resolve(data.myGuilds);
|
deferred.resolve(data.myGuilds);
|
||||||
|
|||||||
@@ -15,10 +15,16 @@ angular.module('habitrpg')
|
|||||||
}
|
}
|
||||||
|
|
||||||
//@TODO: Add paging
|
//@TODO: Add paging
|
||||||
function getGroupMembers (groupId) {
|
function getGroupMembers (groupId, includeAllPublicFields) {
|
||||||
|
var url = apiV3Prefix + '/groups/' + groupId + '/members';
|
||||||
|
|
||||||
|
if (includeAllPublicFields) {
|
||||||
|
url += '?includeAllPublicFields=true';
|
||||||
|
}
|
||||||
|
|
||||||
return $http({
|
return $http({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: apiV3Prefix + '/groups/' + groupId + '/members',
|
url: url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ angular.module('habitrpg')
|
|||||||
tasks.forEach(function (element, index, array) {
|
tasks.forEach(function (element, index, array) {
|
||||||
user[element.type + 's'].push(element)
|
user[element.type + 's'].push(element)
|
||||||
})
|
})
|
||||||
|
|
||||||
save();
|
save();
|
||||||
$rootScope.$emit('userSynced');
|
$rootScope.$emit('userSynced');
|
||||||
});
|
});
|
||||||
@@ -322,7 +322,7 @@ angular.module('habitrpg')
|
|||||||
hourglassPurchase: function (data) {
|
hourglassPurchase: function (data) {
|
||||||
var type = data.params.type;
|
var type = data.params.type;
|
||||||
var key = data.params.key;
|
var key = data.params.key;
|
||||||
callOpsFunctionAndRequest('hourglassPurchase', 'purchase-hourglass', "POST", type + '/' + key, data);
|
callOpsFunctionAndRequest('purchaseHourglass', 'purchase-hourglass', "POST", type + '/' + key, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
unlock: function (data) {
|
unlock: function (data) {
|
||||||
@@ -403,17 +403,10 @@ angular.module('habitrpg')
|
|||||||
settings.online = true;
|
settings.online = true;
|
||||||
save();
|
save();
|
||||||
sync().then(function () {
|
sync().then(function () {
|
||||||
if (cb) {
|
if (user.preferences.timezoneOffset !== offset)
|
||||||
cb();
|
userServices.set({'preferences.timezoneOffset': offset});
|
||||||
}
|
if (cb) cb();
|
||||||
});
|
});
|
||||||
//@TODO: Do we need the timezone set?
|
|
||||||
// userServices.log({}, function(){
|
|
||||||
// // If they don't have timezone, set it
|
|
||||||
// if (user.preferences.timezoneOffset !== offset)
|
|
||||||
// userServices.set({'preferences.timezoneOffset': offset});
|
|
||||||
// cb && cb();
|
|
||||||
// });
|
|
||||||
} else {
|
} else {
|
||||||
alert('Please enter your ID and Token in settings.')
|
alert('Please enter your ID and Token in settings.')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ api.getGroups = {
|
|||||||
if (validationErrors) throw validationErrors;
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
let types = req.query.type.split(',');
|
let types = req.query.type.split(',');
|
||||||
let groupFields = basicGroupFields.concat('description memberCount balance');
|
let groupFields = basicGroupFields.concat(' description memberCount balance');
|
||||||
let sort = '-memberCount';
|
let sort = '-memberCount';
|
||||||
|
|
||||||
let results = await Group.getGroups({user, types, groupFields, sort});
|
let results = await Group.getGroups({user, types, groupFields, sort});
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ script(type='text/ng-template', id='partials/options.settings.api.html')
|
|||||||
h6=env.t('userId')
|
h6=env.t('userId')
|
||||||
pre.prettyprint {{user.id}}
|
pre.prettyprint {{user.id}}
|
||||||
h6=env.t('APIToken')
|
h6=env.t('APIToken')
|
||||||
pre.prettyprint {{user.apiToken}}
|
pre.prettyprint {{User.settings.auth.apiToken}}
|
||||||
h6=env.t('qrCode')
|
h6=env.t('qrCode')
|
||||||
img.img-rendering-auto(src='https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=%7B%22address%22%3A%22https%3A%2F%2Fhabitrpg.com%22%2C%22user%22%3A%22{{user.id}}%22%2C%22key%22%3A%22{{user.apiToken}}%22%7D&choe=UTF-8&chld=L', alt='qrcode')
|
img.img-rendering-auto(src='https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=%7B%22address%22%3A%22https%3A%2F%2Fhabitrpg.com%22%2C%22user%22%3A%22{{user.id}}%22%2C%22key%22%3A%22{{user.apiToken}}%22%7D&choe=UTF-8&chld=L', alt='qrcode')
|
||||||
br
|
br
|
||||||
@@ -405,4 +405,3 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
|
|||||||
.col-xs-4
|
.col-xs-4
|
||||||
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})")
|
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})")
|
||||||
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
|
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
|
||||||
|
|
||||||
|
|||||||
@@ -199,10 +199,10 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
|
|||||||
p!=env.t('prizeValue', {gemcount: "{{challenge.prize}}", gemicon: "<span class='inline-block Pet_Currency_Gem1x'></span>"})
|
p!=env.t('prizeValue', {gemcount: "{{challenge.prize}}", gemicon: "<span class='inline-block Pet_Currency_Gem1x'></span>"})
|
||||||
li.bg-transparent
|
li.bg-transparent
|
||||||
// leave / join
|
// leave / join
|
||||||
a.btn.btn-sm.btn-danger(ng-show='challenge._isMember', ng-click='clickLeave(challenge, $event)')
|
a.btn.btn-sm.btn-danger(ng-show='::isUserMemberOf(challenge)', ng-click='clickLeave(challenge, $event)')
|
||||||
span.glyphicon.glyphicon-ban-circle
|
span.glyphicon.glyphicon-ban-circle
|
||||||
=env.t('leave')
|
=env.t('leave')
|
||||||
a.btn.btn-sm.btn-success(ng-hide='challenge._isMember', ng-click='join(challenge)')
|
a.btn.btn-sm.btn-success(ng-hide='::isUserMemberOf(challenge)', ng-click='join(challenge)')
|
||||||
span.glyphicon.glyphicon-ok
|
span.glyphicon.glyphicon-ok
|
||||||
=env.t('join')
|
=env.t('join')
|
||||||
a.accordion-toggle(id="{{challenge._id}}" ng-click='toggle(challenge._id)')
|
a.accordion-toggle(id="{{challenge._id}}" ng-click='toggle(challenge._id)')
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ script(type='text/ng-template', id='partials/options.social.guilds.public.html')
|
|||||||
li='{{::group.memberCount}} ' + env.t('members')
|
li='{{::group.memberCount}} ' + env.t('members')
|
||||||
// join / leave
|
// join / leave
|
||||||
li.bg-transparent
|
li.bg-transparent
|
||||||
a.btn.btn-sm.btn-danger(ng-if="::group._isMember", ng-click='clickLeave(group, $event)')
|
a.btn.btn-sm.btn-danger(ng-if="::isMemberOfGroup(User.user._id, group)", ng-click='clickLeave(group, $event)')
|
||||||
span.glyphicon.glyphicon-ban-circle
|
span.glyphicon.glyphicon-ban-circle
|
||||||
=env.t('leave')
|
=env.t('leave')
|
||||||
a.btn.btn-sm.btn-success(ng-if="::!group._isMember", ng-click='join(group)')
|
a.btn.btn-sm.btn-success(ng-if="::!isMemberOfGroup(User.user._id, group)", ng-click='join(group)')
|
||||||
span.glyphicon.glyphicon-ok
|
span.glyphicon.glyphicon-ok
|
||||||
=env.t('join')
|
=env.t('join')
|
||||||
h4: a(href='/#/options/groups/guilds/{{::group._id}}') {{::group.name}}
|
h4: a(href='/#/options/groups/guilds/{{::group._id}}') {{::group.name}}
|
||||||
|
|||||||
Reference in New Issue
Block a user