diff --git a/Gruntfile.js b/Gruntfile.js index 428d28c856..c954e18d31 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -38,13 +38,13 @@ module.exports = function(grunt) { 'public/js/services/sharedServices.js', 'public/js/services/userServices.js', 'public/js/services/groupServices.js', + 'public/js/services/memberServices.js', 'public/js/filters/filters.js', 'public/js/directives/directives.js', 'public/js/controllers/authCtrl.js', - 'public/js/controllers/characterCtrl.js', 'public/js/controllers/menuCtrl.js', 'public/js/controllers/notificationCtrl.js', 'public/js/controllers/rootCtrl.js', diff --git a/package.json b/package.json index 3c683eaa82..5e24f91961 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "scripts": { "test": "mocha test/api.mocha.coffee", + "start": "grunt run:dev", "postinstall": "./node_modules/bower/bin/bower install -f" } } diff --git a/public/js/app.js b/public/js/app.js index 4a484933eb..8b2ceb2715 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,7 +1,7 @@ "use strict"; window.habitrpg = angular.module('habitrpg', - ['ngRoute', 'ngResource', 'ngSanitize', 'userServices', 'groupServices', 'sharedServices', 'authServices', 'notificationServices', 'ui.bootstrap', 'ui.keypress']) + ['ngRoute', 'ngResource', 'ngSanitize', 'userServices', 'groupServices', 'memberServices', 'sharedServices', 'authServices', 'notificationServices', 'ui.bootstrap', 'ui.keypress']) .constant("API_URL", "") .constant("STORAGE_USER_ID", 'habitrpg-user') diff --git a/public/js/controllers/characterCtrl.js b/public/js/controllers/characterCtrl.js deleted file mode 100644 index 52a0f9e5a4..0000000000 --- a/public/js/controllers/characterCtrl.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -/** - * The character controller: - * - */ - -habitrpg.controller('CharacterCtrl', - ['$scope', '$location', 'User', - function($scope, $location, User) { - - $scope.user = User.user; - - $scope.equipped = function(user, type) { - var tier = (user.backer && user.backer.tier) - return window.habitrpgShared.helpers.equipped(type, user.items[type], user.preferences, tier); - } - - $scope.$watch('user.tasks', function(){ - $scope.hpPercent = function(hp) { - return (hp / 50) * 100; - } - - $scope.expPercent = function(exp, level) { - return (exp / window.habitrpgShared.algos.tnl(level)) * 100; - } - }) - - $scope.floor = Math.floor; - $scope.count = function(arr) { - return _.size(arr); - } - $scope.tnl = window.habitrpgShared.algos.tnl; - - $scope.showUserAvatar = function() { - $('.userAvatar').show() - } - - } -]); diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 7b57533d62..d2c4463c82 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -1,17 +1,38 @@ "use strict"; -habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'API_URL', '$q', 'User', - function($scope, $rootScope, Groups, $http, API_URL, $q, User) { +habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'API_URL', '$q', 'User', 'Members', '$location', + function($scope, $rootScope, Groups, $http, API_URL, $q, User, Members, $location) { $scope.isMember = function(user, group){ return ~(group.members.indexOf(user._id)); } - $scope.groups = Groups.groups; + // ------ Loading ------ + $scope.groups = Groups.groups; $scope.fetchGuilds = Groups.fetchGuilds; $scope.fetchTavern = Groups.fetchTavern; + // ------ Modals ------ + + $scope.clickMember = function(uid) { + if (User.user._id == uid) { + if ($location.path() == '/tasks') { + $location.path('/options'); + } else { + $location.path('/tasks'); + } + } else { + // We need the member information up top here, but then we pass it down to the modal controller + // down below. Better way of handling this? + debugger + Members.selectMember(uid); + $rootScope.modals.member = true; + } + } + + // ------ Invites ------ + $scope.invitee = ''; $scope.invite = function(group, uuid){ group.$invite({uuid:uuid}, function(){ @@ -22,6 +43,15 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A } ]) + .controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', + function($scope, $rootScope, Members) { + // We watch Members.selectedMember because it's asynchronously set, so would be a hassle to handle updates here + $scope.$watch( function() { return Members.selectedMember; }, function (member) { + $scope.profile = member; + }); + } + ]) + .controller('ChatCtrl', ['$scope', 'Groups', 'User', function($scope, Groups, User){ $scope._chatMessage = ''; $scope.postChat = function(group, message){ diff --git a/public/js/controllers/userAvatarCtrl.js b/public/js/controllers/userAvatarCtrl.js index 2e6cd4ef17..c8f2a0b0a8 100644 --- a/public/js/controllers/userAvatarCtrl.js +++ b/public/js/controllers/userAvatarCtrl.js @@ -6,17 +6,4 @@ habitrpg.controller("UserAvatarCtrl", ['$scope', '$location', 'User', $scope.hideUserAvatar = function() { $(".userAvatar").hide(); }; - - $scope.clickAvatar = function(profile) { - if (User.user.id == profile.id) { - if ($location.path() == '/tasks') { - $location.path('/options'); - } else { - $location.path('/tasks'); - } - } else { - //TODO show party member modal - //$("#avatar-modal-#{uid}").modal('show') - } - } }]); diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 9aa707d1f8..5dfbf7aa56 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -5,8 +5,8 @@ */ angular.module('groupServices', ['ngResource']). - factory('Groups', ['API_URL', '$resource', 'User', '$q', - function(API_URL, $resource, User, $q) { + factory('Groups', ['API_URL', '$resource', 'User', '$q', 'Members', + function(API_URL, $resource, User, $q, Members) { var Group = $resource(API_URL + '/api/v1/groups/:gid', {gid:'@_id'}, { @@ -34,6 +34,7 @@ angular.module('groupServices', ['ngResource']). // But we don't defer triggering Party, since we always need it for the header if nothing else Group.query({type:'party'}, function(_groups){ partyQ.resolve(_groups[0]); + Members.populate(_groups[0]); }) return { @@ -42,10 +43,12 @@ angular.module('groupServices', ['ngResource']). $('#loading-indicator').show(); Group.query({type:'guilds'}, function(_groups){ guildsQ.resolve(_groups); + Members.populate(_groups); $('#loading-indicator').hide(); }) Group.query({type:'public'}, function(_groups){ publicQ.resolve(_groups); + Members.populate(_groups); }) }), @@ -54,6 +57,7 @@ angular.module('groupServices', ['ngResource']). Group.query({type:'tavern'}, function(_groups){ $('#loading-indicator').hide(); tavernQ.resolve(_groups[0]); + Members.populate(_groups[0]); }) }), diff --git a/public/js/services/memberServices.js b/public/js/services/memberServices.js new file mode 100644 index 0000000000..2cfec887e9 --- /dev/null +++ b/public/js/services/memberServices.js @@ -0,0 +1,73 @@ +'use strict'; + +/** + * Services that persists and retrieves user from localStorage. + */ + +angular.module('memberServices', ['ngResource']). + factory('Members', ['API_URL', '$resource', + function(API_URL, $resource) { + var members = {}; + var Member = $resource(API_URL + '/api/v1/members/:uid', {uid:'@_id'}); + return { + + members: members, + + /** + * Allows us to lazy-load party / group / public members throughout the application. + * @param obj - either a group or an individual member. If it's a group, we lazy-load all of its members. + */ + populate: function(obj){ + + function populateGroup(group){ + _.each(group.members, function(member){ + // meaning `populate('members')` wasn't run on the server, so we're getting the "in-database" form of + // the members array, which is just a list of IDs - not the populated objects + if (_.isString(member)) return; + + // lazy-load + if (!members[member._id]) members[member._id] = member; + }) + } + + // Array of groups + if (_.isArray(obj)) { + if (obj[0] && obj[0].members) { + _.each(obj, function(group){ + populateGroup(group); + }) + } + + // Individual Group + } else if (obj.members) + populateGroup(obj); + + // individual Member + if (obj._id) { + if (!members[obj._id]) { + members[obj._id] = obj; + } + } + }, + + selectedMember: undefined, + + /** + * Once users are populated, we fetch them throughout the application (eg, modals). This + * either gets them or fetches if not available + * @param uid + */ + selectMember: function(uid) { + var self = this; + if (members[uid]) { + self.selectedMember = members[uid]; + } else { + Member.get({uid: uid}, function(member){ + self.populate(member); // lazy load for later + self.selectedMember = members[member._id]; + }); + } + } + } + } +]); diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 4c17abcdd5..8e2372e6b5 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -19,6 +19,14 @@ var api = module.exports; var usernameFields = 'auth.local.username auth.facebook.displayName auth.facebook.givenName auth.facebook.familyName auth.facebook.name'; var partyFields = 'profile preferences items stats achievements party backer flags.rest ' + usernameFields; +api.getMember = function(req, res) { + User.findById(req.params.uid).select(partyFields).exec(function(err, user){ + if (err) return res.json(500,{err:err}); + if (!user) return res.json(400,{err:'User not found'}); + res.json(user); + }) +} + /** * Get groups. If req.query.type privided, returned as an array (so ngResource can use). If not, returned as * object {guilds, public, party, tavern}. req.query.type can be comma-separated `type=guilds,party` diff --git a/src/routes/api.js b/src/routes/api.js index 493b39745e..df595bc43c 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -64,6 +64,9 @@ router.post('/groups/:gid/chat', auth.auth, groups.attachGroup, groups.postChat) //PUT /groups/:gid/chat/:messageId //DELETE /groups/:gid/chat/:messageId +/* Members */ +router.get('/members/:uid', groups.getMember); + // Market router.post('/market/buy', auth.auth, user.marketBuy); diff --git a/views/index.jade b/views/index.jade index 55b3596a70..512bcc3738 100644 --- a/views/index.jade +++ b/views/index.jade @@ -52,13 +52,13 @@ html script(type='text/javascript', src='/js/services/sharedServices.js') script(type='text/javascript', src='/js/services/userServices.js') script(type='text/javascript', src='/js/services/groupServices.js') + script(type='text/javascript', src='/js/services/memberServices.js') script(type='text/javascript', src='/js/filters/filters.js') script(type='text/javascript', src='/js/directives/directives.js') script(type='text/javascript', src='/js/controllers/authCtrl.js') - script(type='text/javascript', src='/js/controllers/characterCtrl.js') script(type='text/javascript', src='/js/controllers/menuCtrl.js') script(type='text/javascript', src='/js/controllers/notificationCtrl.js') script(type='text/javascript', src='/js/controllers/rootCtrl.js') diff --git a/views/options/groups/chat-message.jade b/views/options/groups/chat-message.jade index 54f1961c34..2871db9144 100644 --- a/views/options/groups/chat-message.jade +++ b/views/options/groups/chat-message.jade @@ -1,5 +1,5 @@ li(ng-repeat='message in group.chat', ng-class='{highlight: message.text.indexOf(username(user.auth,user.profile.name)) != -1}') - span.label.chat-message(class='{{nameTagClasses(message)}}', tooltip='{{message.contributor}}') + a.label.chat-message(class='{{nameTagClasses(message)}}', tooltip='{{message.contributor}}', ng-click='clickMember(message.uuid)') | {{message.user}} span span(ng-bind-html="message.text | linky:'_blank'") - diff --git a/views/options/groups/index.jade b/views/options/groups/index.jade index 573edf3a68..31c8745f7d 100644 --- a/views/options/groups/index.jade +++ b/views/options/groups/index.jade @@ -1,68 +1,66 @@ -div#groups-controller(ng-controller='GroupsCtrl') +// FIXME note, due to https://github.com/angular-ui/bootstrap/issues/783 we can't use nested angular-bootstrap tabs +// Subscribe to that ticket & change this when they fix - // FIXME note, due to https://github.com/angular-ui/bootstrap/issues/783 we can't use nested angular-bootstrap tabs - // Subscribe to that ticket & change this when they fix +ul.nav.nav-tabs + li.active + a(data-target='#groups-party', data-toggle='tab') Party + li + a(data-target='#groups-guilds', data-toggle='tab', ng-click='fetchGuilds()') Guilds - ul.nav.nav-tabs - li.active - a(data-target='#groups-party', data-toggle='tab') Party - li - a(data-target='#groups-guilds', data-toggle='tab', ng-click='fetchGuilds()') Guilds +.tab-content(ng-controller='PartyCtrl') + #groups-party.tab-pane.active + div(ng-show='group._id') + include ./group + div(ng-hide='group._id') + div(ng-show='user.invitations.party') + // #with required for the accept/reject buttons + // {#with _user.invitations.party as :party} + h2 You're Invited To {{user.invitations.party.name}} + a.btn.btn-success(data-type='party', ng-click='join(user.invitations.party)') Accept + a.btn.btn-danger(ng-click='reject(group)') Reject + // {/} + div(ng-hide='user.invitations.party', ng-controller='PartyCtrl') + h2 Create A Party + p + | You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter: + pre.prettyprint. + {{user.id}} + include ./create-group - .tab-content(ng-controller='PartyCtrl') - #groups-party.tab-pane.active - div(ng-show='group._id') + #groups-guilds.tab-pane(ng-controller='GuildsCtrl') + ul.nav.nav-tabs + li.active + a(data-target='#groups-public-guilds', data-toggle='tab') Public Guilds + li(ng-repeat='group in groups.guilds') + a(data-target='#groups-guild-{{group._id}}', data-toggle='tab') {{group.name}} + li + a(data-target='#groups-create-guild', data-toggle='tab') Create Guild + .tab-content + .tab-pane.active#groups-public-guilds + div(ng-repeat='invitation in user.invitations.guilds') + h3 You're Invited To {{invitation.name}} + a.btn.btn-success(data-type='guild', ng-click='join(group)') Accept + a.btn.btn-danger(x-bind='click:rejectInvitation') Reject + // Public Groups + .options-group.option-large.whatever-options + input.option-content(type='text',ng-model='guildSearch', placeholder='Search') + table.table.table-striped + tr(ng-repeat='group in groups.public | filter:guildSearch') + td + ul.pull-right.challenge-accordion-header-specs + li {{group.members.length}} member(s) + li + // join / leave + a.btn.btn-small.btn-danger(ng-show='isMember(user, group)', ng-click='leave(group)') + i.icon-ban-circle + | Leave + a.btn.btn-small.btn-success(ng-hide='isMember(user, group)', ng-click='join(group)') + i.icon-ok + | Join + h4 {{group.name}} + p {{group.description}} + .tab-pane(id='groups-guild-{{group._id}}', ng-repeat='group in groups.guilds') include ./group - div(ng-hide='group._id') - div(ng-show='user.invitations.party') - // #with required for the accept/reject buttons - // {#with _user.invitations.party as :party} - h2 You're Invited To {{user.invitations.party.name}} - a.btn.btn-success(data-type='party', ng-click='join(user.invitations.party)') Accept - a.btn.btn-danger(ng-click='reject(group)') Reject - // {/} - div(ng-hide='user.invitations.party', ng-controller='PartyCtrl') - h2 Create A Party - p - | You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter: - pre.prettyprint. - {{user.id}} - include ./create-group - #groups-guilds.tab-pane(ng-controller='GuildsCtrl') - ul.nav.nav-tabs - li.active - a(data-target='#groups-public-guilds', data-toggle='tab') Public Guilds - li(ng-repeat='group in groups.guilds') - a(data-target='#groups-guild-{{group._id}}', data-toggle='tab') {{group.name}} - li - a(data-target='#groups-create-guild', data-toggle='tab') Create Guild - .tab-content - .tab-pane.active#groups-public-guilds - div(ng-repeat='invitation in user.invitations.guilds') - h3 You're Invited To {{invitation.name}} - a.btn.btn-success(data-type='guild', ng-click='join(group)') Accept - a.btn.btn-danger(x-bind='click:rejectInvitation') Reject - // Public Groups - .options-group.option-large.whatever-options - input.option-content(type='text',ng-model='guildSearch', placeholder='Search') - table.table.table-striped - tr(ng-repeat='group in groups.public | filter:guildSearch') - td - ul.pull-right.challenge-accordion-header-specs - li {{group.members.length}} member(s) - li - // join / leave - a.btn.btn-small.btn-danger(ng-show='isMember(user, group)', ng-click='leave(group)') - i.icon-ban-circle - | Leave - a.btn.btn-small.btn-success(ng-hide='isMember(user, group)', ng-click='join(group)') - i.icon-ok - | Join - h4 {{group.name}} - p {{group.description}} - .tab-pane(id='groups-guild-{{group._id}}', ng-repeat='group in groups.guilds') - include ./group - - .tab-pane#groups-create-guild - include ./create-group \ No newline at end of file + .tab-pane#groups-create-guild + include ./create-group \ No newline at end of file diff --git a/views/shared/header/avatar.jade b/views/shared/header/avatar.jade index 4f61cf3ff1..1ba4dbd22d 100644 --- a/views/shared/header/avatar.jade +++ b/views/shared/header/avatar.jade @@ -1,4 +1,4 @@ -figure.herobox(ng-click='clickAvatar(profile)', data-name='{{username(profile.auth, profile.profile.name)}}', ng-class='{isUser: profile.id==user.id, hasPet: profile.items.pet}', data-level='{{profile.stats.lvl}}', data-uid='{{profile.id}}', rel='popover', data-placement='bottom', data-trigger='hover', data-html='true', data-content="
Level: {{profile.stats.lvl}}
GP: {{profile.stats.gp | number:0}}
{{count(profile.items.pets)}} / 90 Pets Found
") +figure.herobox(ng-click='clickMember(profile._id)', data-name='{{username(profile.auth, profile.profile.name)}}', ng-class='{isUser: profile.id==user.id, hasPet: profile.items.pet}', data-level='{{profile.stats.lvl}}', data-uid='{{profile.id}}', rel='popover', data-placement='bottom', data-trigger='hover', data-html='true', data-content="
Level: {{profile.stats.lvl}}
GP: {{profile.stats.gp | number:0}}
{{count(profile.items.pets)}} / 90 Pets Found
") .character-sprites span(ng-class='{zzz:profile.flags.rest}') span(class='{{profile.preferences.gender}}_skin_{{profile.preferences.skin}}') diff --git a/views/shared/modals/index.jade b/views/shared/modals/index.jade index 64152d36c4..8f0bfda362 100644 --- a/views/shared/modals/index.jade +++ b/views/shared/modals/index.jade @@ -4,4 +4,5 @@ include ./reroll include ./death include ./new-stuff include ./why-ads -include ./more-gems \ No newline at end of file +include ./more-gems +include ./members \ No newline at end of file diff --git a/views/shared/modals/members.html b/views/shared/modals/members.html index 0561e77c92..f6f2d74d88 100644 --- a/views/shared/modals/members.html +++ b/views/shared/modals/members.html @@ -1,40 +1,27 @@ -{{#each _membersArray as :member}} - - +
+
+ - - -

{{username(@profile.auth, @profile.profile.name)}}

+ +

{{username(profile.auth, profile.profile.name)}}


- {{#if @profile.profile.imageUrl}} - - {{/}} - {{#if @profile.profile.blurb}} -

{{@profile.profile.blurb}}

- {{/}} - {{#if @profile.profile.websites}} -
    - {{#each @profile.profile.websites as :website}} -
  • {{:website}}
  • - {{/}} + +

    {{profile.profile.blurb}}

    +
      +
    • {{website}}
    - {{/}}

    Stats

    - +

Achievements

- - +
- - - - <@footer> - - - -{{/}} \ No newline at end of file + +
+
diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade new file mode 100644 index 0000000000..45382651a9 --- /dev/null +++ b/views/shared/modals/members.jade @@ -0,0 +1,21 @@ +div(ng-controller='MemberModalCtrl') + div(modal='modals.member') + //-.modal-header + h3 username + .modal-body + h2.profile-modal-header {{username(profile.auth, profile.profile.name)}} + app:avatar:profile + hr + .row-fluid + .span6 + img(ng-show='profile.profile.imageUrl', ng-src='{{profile.profile.imageUrl}}') + p(ng-show='profile.profile.blurb') {{profile.profile.blurb}} + ul(ng-show='profile.profile.websites') + li(ng-repeat='website in profile.profile.websites') {{website}} + h3 Stats + app:avatar:profile-stats + .span6 + h3 Achievements + app:avatar:achievements + .modal-footer + button.btn.btn-default(ng-click='modals.member = false') Ok