mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-08-02 07:49:39 +00:00
feat(private-messages): start implementing private messaging (includes blocking). Start experimenting with reflists for PMs, instead of arrays
This commit is contained in:
parent
b36c9c0fb0
commit
f5415ac6c5
14 changed files with 189 additions and 55 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
57
src/controllers/members.js
Normal file
57
src/controllers/members.js
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -160,5 +160,4 @@
|
|||
|
|
||||
=env.t('communityGuidelinesRead2')
|
||||
|
||||
ul.list-unstyled.tavern-chat
|
||||
include ./chat-message
|
||||
+chatMessages()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
Loading…
Reference in a new issue