feat(private-messages): start implementing private messaging (includes blocking). Start experimenting with reflists for PMs, instead of arrays

This commit is contained in:
Tyler Renelle 2014-11-19 13:53:47 -08:00
parent b36c9c0fb0
commit f5415ac6c5
14 changed files with 189 additions and 55 deletions

View file

@ -75,6 +75,16 @@ window.habitrpg = angular.module('habitrpg',
templateUrl: "partials/options.social.html"
})
.state('options.social.inbox', {
url: "/inbox",
templateUrl: "partials/options.social.inbox.html",
controller: ['$rootScope', function($rootScope){
// clear "new messages"
//$rootScope.$on('userSynced',function(){
$rootScope.User.user.ops.update && $rootScope.set({'inbox.newMessages':0});
}]
})
.state('options.social.tavern', {
url: "/tavern",
templateUrl: "partials/options.social.tavern.html",

View file

@ -101,8 +101,8 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}
])
.controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared',
function($scope, $rootScope, Members, Shared) {
.controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification',
function($scope, $rootScope, Members, Shared, $http, Notification) {
$scope.timestamp = function(timestamp){
return moment(timestamp).format('MM/DD/YYYY');
}
@ -112,6 +112,13 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
member.petCount = Shared.countPets(null, member.items.pets);
$scope.profile = member;
});
$scope.sendPrivateMessage = function(uuid, message){
$http.post('/api/v2/members/'+uuid+'/message',{message:message}).success(function(){
Notification.text('Message sent.');
$rootScope.User.sync();
$scope.$close();
});
}
}
])

View file

@ -19,7 +19,7 @@ var api = module.exports;
------------------------------------------------------------------------
*/
var partyFields = 'profile preferences stats achievements party backer contributor auth.timestamps items';
var partyFields = api.partyFields = 'profile preferences stats achievements party backer contributor auth.timestamps items';
var nameFields = 'profile.name';
var challengeFields = '_id name';
var guildPopulate = {path: 'members', select: nameFields, options: {limit: 15} };
@ -44,15 +44,6 @@ var populateQuery = function(type, q){
return q;
}
api.getMember = function(req, res, next) {
User.findById(req.params.uid).select(partyFields).exec(function(err, user){
if (err) return next(err);
if (!user) return res.json(400,{err:'User not found'});
res.json(user);
})
}
/**
* Fetch groups list. This no longer returns party or tavern, as those can be requested indivdually
* as /groups/party or /groups/tavern

View file

@ -0,0 +1,57 @@
var User = require('mongoose').model('User');
var groups = require('../models/group');
var partyFields = require('./groups').partyFields
var api = module.exports;
var async = require('async');
var _ = require('lodash');
var shared = require('habitrpg-shared');
api.getMember = function(req, res, next) {
User.findById(req.params.uuid).select(partyFields).exec(function(err, user){
if (err) return next(err);
if (!user) return res.json(400,{err:'User not found'});
res.json(user);
})
}
api.sendPrivateMessage = function(req,res,next){
async.waterfall([
function(cb){
User.findById(req.params.uuid, cb);
},
function(member, cb){
if (!member) return cb({code:404, err: 'User not found'});
if (~member.inbox.blocks.indexOf(res.locals.user._id) || member.inbox.optOut) {
return cb({code:401, err: "Can't send message to this user."});
}
var message = groups.chatDefaults(req.body.message, res.locals.user);
shared.refPush(member.inbox.messages, message);
member.inbox.newMessages++;
member._v++;
member.markModified('inbox.messages');
var message = groups.chatDefaults(req.body.message, member);
shared.refPush(res.locals.user.inbox.messages, _.defaults({sent:true},message));
res.locals.user.markModified('inbox.messages');
member.save(cb);
},
function(a,b,cb){
res.locals.user.save(cb);
}
], function(err){
if (err) return err.code ? res.json(err.code,{err:err.err}) : err;
res.send(200);
})
}
api.block = function(req,res,next){
var b = res.locals.user.inbox;
if (~b.blocks.indexOf(req.params.id)){
b.blocks.push(req.params.id)
res.locals.user.save();
}
res.send(200);
}

View file

@ -216,7 +216,7 @@ api.getUser = function(req, res, next) {
* FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations
*/
acceptablePUTPaths = _.reduce(require('./../models/user').schema.paths, function(m,v,leaf){
var found= _.find('achievements filters flags invitations lastCron party preferences profile stats'.split(' '), function(root){
var found= _.find('achievements filters flags invitations lastCron party preferences profile stats inbox'.split(' '), function(root){
return leaf.indexOf(root) == 0;
});
if (found) m[leaf]=true;

View file

@ -93,7 +93,7 @@ GroupSchema.methods.toJSON = function(){
return doc;
}
var chatDefaults = function(message,user){
var chatDefaults = module.exports.chatDefaults = function(message,user){
var message = {
id: shared.uuid(),
text: message,

View file

@ -326,6 +326,13 @@ var UserSchema = new Schema({
challenges: [{type: 'String', ref:'Challenge'}],
inbox: {
newMessages: {type:Number, 'default':0},
blocks: {type:Array, 'default':[]},
messages: {type:Schema.Types.Mixed, 'default':{}}, //reflist
optOut: {type:Boolean, 'default':false}
},
habits: {type:[TaskSchemas.HabitSchema]},
dailys: {type:[TaskSchemas.DailySchema]},
todos: {type:[TaskSchemas.TodoSchema]},

View file

@ -10,6 +10,7 @@ $ mocha test/user.mocha.coffee
user = require("../controllers/user")
groups = require("../controllers/groups")
members = require("../controllers/members")
auth = require("../controllers/auth")
hall = require("../controllers/hall")
challenges = require("../controllers/challenges")
@ -579,9 +580,28 @@ module.exports = (swagger, v2) ->
# ---------------------------------
# Members
# ---------------------------------
"/members/{uid}":
"/members/{uuid}":
spec:{}
action: groups.getMember
action: members.getMember
"/members/{uuid}/message":
spec:
method: 'POST'
description: 'Send a private message to a member'
parameters: [
path 'uuid', 'The UUID of the member to message', 'string'
body '', '{message: "The private message to send"}', 'object'
]
middleware: [auth.auth]
action: members.sendPrivateMessage
"/members/{uuid}/block":
spec:
method: 'POST'
description: 'Block a member from sending private messages'
parameters: [
path 'uuid', 'The UUID of the member to message', 'string'
]
middleware: [auth.auth]
action: members.block
# ---------------------------------
# Hall of Heroes / Patrons

View file

@ -1,19 +1,26 @@
li.chat-message(ng-repeat='message in group.chat track by message.id', ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}')
.scrollable-message
span(ng-if='::message.user')
a.label.label-default.chat-message.hidden-label
span {{::message.user}} 
span(ng-class='userAdminGlyphiconStyleFromLevel(message.contributor.level)')
// this invisible username label is here to push the message text far enough right that the visible label can be floated to this point without covering up any of the text
markdown(ng-model='::message.text')
| -
span.muted.time(from-now='::message.timestamp')
span
a.label.label-default(ng-show='countExists(message.likes)', ng-class='{"label-success":message.likes[user._id]}', ng-click='likeChatMessage(group,message)') +{{countExists(message.likes)}}
a.chat-plus-one.muted(ng-show='!countExists(message.likes)', ng-click='likeChatMessage(group,message)') +1
|  
a(ng-if=':: user.contributor.admin || message.uuid == user.id', ng-click='deleteChatMessage(group, message)')
span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
a.label.label-default.chat-message(ng-if=':: message.user', class='float-label', ng-class='userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)')
span(tooltip='{{::contribText(message.contributor, message.backer)}}') {{::message.user}} 
span(ng-class='userAdminGlyphiconStyleFromLevel(message.contributor.level)')
mixin chatMessages(inbox)
ul.list-unstyled.tavern-chat
- var ngRepeat = inbox ? 'message in user.inbox.messages | toArray:true | orderBy:"sort":true' : 'message in group.chat track by message.id'
li.chat-message(ng-repeat=ngRepeat, ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}', style='{{::message.sent ? "opacity:0.5" : ""}}')
.scrollable-message
span(ng-if='::message.user')
a.label.label-default.chat-message.hidden-label
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
span {{::message.user}} 
span(ng-class='userAdminGlyphiconStyleFromLevel(message.contributor.level)')
// this invisible username label is here to push the message text far enough right that the visible label can be floated to this point without covering up any of the text
markdown(ng-model='::message.text')
| -
span.muted.time(from-now='::message.timestamp')
unless type='inbox'
span
a.label.label-default(ng-show='countExists(message.likes)', ng-class='{"label-success":message.likes[user._id]}', ng-click='likeChatMessage(group,message)') +{{countExists(message.likes)}}
a.chat-plus-one.muted(ng-show='!countExists(message.likes)', ng-click='likeChatMessage(group, message)') +1
|  
a(ng-click='#{inbox? "user.ops.deletePM({params:{id:message.$key}})" : "deleteChatMessage(group, message)"}', ng-if='#{inbox ? "true" : ":: user.contributor.admin || message.uuid == user.id"}')
span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
span.float-label
a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)')
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
span(tooltip='{{::contribText(message.contributor, message.backer)}}') {{::message.user}} 
span(ng-class='::userAdminGlyphiconStyleFromLevel(message.contributor.level)')

View file

@ -114,5 +114,4 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
h3=env.t('chat')
include ./chat-box
ul.list-unstyled.tavern-chat
include ./chat-message
+chatMessages()

View file

@ -1,9 +1,20 @@
// 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
include ./challenges.jade
include ./hall.jade
include ./boss.jade
include ./challenges
include ./hall
include ./boss
include ./chat-message
script(type='text/ng-template', id='partials/options.social.inbox.html')
.container-fluid
.row
.col-md-12
+chatMessages('inbox')
.checkbox
label
input(type='checkbox', ng-model='user.inbox.optOut', ng-change='set({"inbox.optOut": user.inbox.optOut?true: false})')
span.hint(popover-trigger='mouseenter', popover-placement='right', popover="Don't like private messages? Click to completely opt out") Opt Out
script(type='text/ng-template', id='partials/options.social.tavern.html')
include ./tavern
@ -78,6 +89,10 @@ script(type='text/ng-template', id='partials/options.social.guilds.html')
script(type='text/ng-template', id='partials/options.social.html')
ul.options-menu
li(ng-class="{ active: $state.includes('options.social.inbox') }")
a(ui-sref='options.social.inbox')
| Inbox 
span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}}
li(ng-class="{ active: $state.includes('options.social.tavern') }")
a(ui-sref='options.social.tavern')
=env.t('tavern')

View file

@ -160,5 +160,4 @@
|  
=env.t('communityGuidelinesRead2')
ul.list-unstyled.tavern-chat
include ./chat-message
+chatMessages()

View file

@ -24,6 +24,10 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li
a(ui-sref='options.profile.profile')=env.t('profile')
ul.toolbar-submenu
li
a(ui-sref='options.social.inbox')
| Inbox 
span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}}
li
a(ui-sref='options.social.tavern')=env.t('tavern')
li
@ -88,10 +92,15 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li.toolbar-button-dropdown
a(ui-sref='options.social.tavern')
span=env.t('social')
span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}}
a(ng-click='expandMenu("social")', ng-class='{active: _expandedMenu == "social"}')
span ☰
div(ng-if='_expandedMenu == "social"')
ul.toolbar-submenu(ng-click='expandMenu(null)')
li
a(ui-sref='options.social.inbox')
| Inbox 
span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}}
li
a(ui-sref='options.social.tavern')=env.t('tavern')
li

View file

@ -1,28 +1,32 @@
script(type='text/ng-template', id='modals/member.html')
.modal-header(bindonce='profile')
script(type='text/ng-template', id='modals/member.html')
.modal-header
h4
span {{profile.profile.name}}
span(ng-if='profile.contributor.level') - {{contribText(profile.contributor, profile.backer)}}
.modal-body(bindonce='profile')
span {{::profile.profile.name}}
span(ng-if='profile.contributor.level') - {{::contribText(profile.contributor, profile.backer)}}
ul.pull-right.list-inline(ng-if='::user')
li.glyphicon.glyphicon-plus(ng-show='user.inbox.blocks | contains:profile._id', tooltip='Un-block', ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer')
li.glyphicon.glyphicon-remove(ng-hide='user.inbox.blocks | contains:profile._id', tooltip='Block', ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer')
li.glyphicon.glyphicon-envelope(tooltip='Send private message', ng-click="openModal('private-message',{controller:'MemberModalCtrl'})", tooltip-placement='bottom', style='cursor:pointer')
.modal-body
.container-fluid
.row
.col-md-6
img(ng-show='profile.profile.imageUrl', ng-src='{{profile.profile.imageUrl}}')
markdown(ng-show='profile.profile.blurb', ng-model='profile.profile.blurb')
ul.muted.list-unstyled(ng-if='profile.auth.timestamps')
img(ng-show='::profile.profile.imageUrl', ng-src='{{::profile.profile.imageUrl}}')
markdown(ng-show='::profile.profile.blurb', ng-model='::profile.profile.blurb')
ul.muted.list-unstyled(ng-if='::profile.auth.timestamps')
li {{profile._id}}
li(ng-show='profile.auth.timestamps.created')
li(ng-show='::profile.auth.timestamps.created')
| 
=env.t('memberSince')
| 
| {{timestamp(profile.auth.timestamps.created)}} -
li(ng-show='profile.auth.timestamps.loggedin')
| {{::timestamp(profile.auth.timestamps.created)}} -
li(ng-show='::profile.auth.timestamps.loggedin')
| 
=env.t('lastLoggedIn')
| 
| {{timestamp(profile.auth.timestamps.loggedin)}} -
| {{::timestamp(profile.auth.timestamps.loggedin)}} -
h3=env.t('stats')
.label.label-info {{ {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[profile.stats.class] }}
.label.label-info {{:: {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[profile.stats.class] }}
include ../profiles/stats
.col-md-6
+herobox()
@ -30,3 +34,12 @@ script(type='text/ng-template', id='modals/member.html')
include ../profiles/achievements
.modal-footer
button.btn.btn-default(ng-click='$close()')=env.t('ok')
script(type='text/ng-template', id='modals/private-message.html')
.modal-header
h4 Private message to {{profile.profile.name}}
.modal-body
textarea.form-control(type='text',ng-model='_message')
.modal-footer
button.btn.btn-primary(ng-click='sendPrivateMessage(profile._id, _message)') Send
button.btn.btn-default(ng-click='$close()')=env.t('cancel')