From f3fa2f10eda89b5bacc6a74879e5d452484116e8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 7 Jan 2015 13:16:34 -0500 Subject: [PATCH 01/28] added ACP option to enable private groups --- src/views/admin/partials/menu.tpl | 1 + src/views/admin/settings/group.tpl | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/views/admin/settings/group.tpl diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 7059ad6ba0..29f049fa03 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -23,6 +23,7 @@
  • Reputation
  • Email
  • User
  • +
  • Group
  • Guest
  • Post
  • Pagination
  • diff --git a/src/views/admin/settings/group.tpl b/src/views/admin/settings/group.tpl new file mode 100644 index 0000000000..7da62689ce --- /dev/null +++ b/src/views/admin/settings/group.tpl @@ -0,0 +1,22 @@ + + +
    +
    General
    +
    +
    +
    + +
    +
    +
    +
    + + \ No newline at end of file From 917f76feab7a3e74192fc87f8e0e5b9e333ff147 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 7 Jan 2015 14:58:31 -0500 Subject: [PATCH 02/28] group join/leave controls in groups list page (+ helper!) --- public/src/helpers.js | 14 ++++++++++++++ src/controllers/groups.js | 3 ++- src/groups.js | 20 +++++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/public/src/helpers.js b/public/src/helpers.js index a89aeae0e2..38875ea8b8 100644 --- a/public/src/helpers.js +++ b/public/src/helpers.js @@ -17,6 +17,19 @@ return ''; }; + // Groups helpers + helpers.membershipBtn = function(groupObj) { + if (groupObj.isMember) { + return ''; + } else { + if (groupObj.pending) { + return ''; + } else { + return ''; + } + } + }; + if ('undefined' !== typeof window) { $(document).ready(module.exports); } @@ -26,6 +39,7 @@ templates.registerHelper('displayUsersLink', helpers.displayUsersLink); templates.registerHelper('buildMetaTag', helpers.buildMetaTag); + templates.registerHelper('membershipBtn', helpers.membershipBtn); }; })('undefined' === typeof module ? { diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 3843672a09..72d83d4fc2 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -9,7 +9,8 @@ var groups = require('../groups'), groupsController.list = function(req, res, next) { groups.list({ truncateUserList: true, - expand: true + expand: true, + uid: req.user ? req.user.uid : 0 }, function(err, groups) { if (err) { return next(err); diff --git a/src/groups.js b/src/groups.js index 47e7002db9..4f59d39820 100644 --- a/src/groups.js +++ b/src/groups.js @@ -71,7 +71,25 @@ var async = require('async'), groupNames = groupNames.concat(ephemeralGroups); async.map(groupNames, function (groupName, next) { - Groups.get(groupName, options, next); + async.waterfall([ + async.apply(Groups.get, groupName, options), + function(groupObj, next) { + // Retrieve group membership state, if uid is passed in + if (!options.uid) { + return next(null, groupObj); + } + + Groups.isMember(options.uid, groupName, function(err, isMember) { + if (err) { + winston.warn('[groups.list] Could not determine membership in group `' + groupName + '` for uid `' + options.uid + '`: ' + err.message); + return next(null, groupObj); + } + + groupObj.isMember = isMember; + next(null, groupObj); + }); + } + ], next); }, function (err, groups) { callback(err, internals.filterGroups(groups, options)); }); From a2af8105ec56a0de53cbf2de4e06bf2ca0432ce1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 7 Jan 2015 16:18:38 -0500 Subject: [PATCH 03/28] closed #2583 --- public/src/app.js | 5 ++++- public/src/{ => modules}/helpers.js | 35 +++++++++++++++++++---------- src/meta/js.js | 1 - src/webserver.js | 5 ++++- 4 files changed, 31 insertions(+), 15 deletions(-) rename public/src/{ => modules}/helpers.js (65%) diff --git a/public/src/app.js b/public/src/app.js index bf5ec32941..7e1160dee1 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -568,8 +568,11 @@ app.uid = null; } }); - require(['taskbar'], function(taskbar) { + require(['taskbar', 'helpers'], function(taskbar, helpers) { taskbar.init(); + + // templates.js helpers + helpers.register(); }); }); }; diff --git a/public/src/helpers.js b/public/src/modules/helpers.js similarity index 65% rename from public/src/helpers.js rename to public/src/modules/helpers.js index 38875ea8b8..da4eaccaae 100644 --- a/public/src/helpers.js +++ b/public/src/modules/helpers.js @@ -1,8 +1,12 @@ -"use strict"; -/*global templates*/ +;(function(exports) { + "use strict"; + /* globals define */ + // export the class if we are in a Node-like system. + if (typeof module === 'object' && module.exports === exports) { + exports = module.exports/* = SemVer*/; + } -(function(module) { var helpers = {}; helpers.displayUsersLink = function(config) { @@ -30,20 +34,27 @@ } }; - if ('undefined' !== typeof window) { - $(document).ready(module.exports); - } + exports.register = function() { + var templates; - module.exports = function() { - var templates = templates || require('templates.js'); + if (typeof module === 'object') { + templates = require('templates.js'); + } else { + templates = window.templates; + } templates.registerHelper('displayUsersLink', helpers.displayUsersLink); templates.registerHelper('buildMetaTag', helpers.buildMetaTag); templates.registerHelper('membershipBtn', helpers.membershipBtn); }; -})('undefined' === typeof module ? { - module: { - exports: {} + // Use the define() function if we're in AMD land + if (typeof define === 'function' && define.amd) { + define('helpers', exports); } -} : module); + +})( + typeof exports === 'object' ? exports : + typeof define === 'function' && define.amd ? {} : + helpers = {} +); \ No newline at end of file diff --git a/src/meta/js.js b/src/meta/js.js index c653a15de9..a960d108ee 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -45,7 +45,6 @@ module.exports = function(Meta) { 'public/src/variables.js', 'public/src/widgets.js', 'public/src/translator.js', - 'public/src/helpers.js', 'public/src/overrides.js' ], rjs: [] diff --git a/src/webserver.js b/src/webserver.js index 8d905b99b6..01dee2b91c 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -18,7 +18,7 @@ var path = require('path'), routes = require('./routes'), emitter = require('./emitter'), - helpers = require('./../public/src/helpers')(), + helpers = require('./../public/src/modules/helpers'), net; if(nconf.get('ssl')) { @@ -48,6 +48,9 @@ if(nconf.get('ssl')) { middleware = middleware(app); routes(app, middleware); + // Load server-side template helpers + helpers.register(); + // Cache static files on production if (global.env !== 'development') { app.enable('cache'); From 501935d359315a40fb3e655cc27342498b32b23b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 7 Jan 2015 16:38:23 -0500 Subject: [PATCH 04/28] updated helper loading so it loads all helpers in helpers local var, #2588 --- public/src/modules/helpers.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index 1d13f93bf7..838ad7a4da 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -43,8 +43,9 @@ templates = window.templates; } - templates.registerHelper('displayUsersLink', helpers.displayUsersLink); - templates.registerHelper('buildMetaTag', helpers.buildMetaTag); + Object.keys(helpers).forEach(function(helperName) { + templates.registerHelper(helperName, helpers[helperName]); + }); }; // Use the define() function if we're in AMD land From 9fa3675424a58efc7de7cb2fb66659e45c62348d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 7 Jan 2015 16:55:14 -0500 Subject: [PATCH 05/28] added methods and socket listeners for group joining and leaving, #2588 --- public/src/client/groups/list.js | 26 ++++++++++++++++++++++++++ src/socket.io/groups.js | 23 +++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 public/src/client/groups/list.js create mode 100644 src/socket.io/groups.js diff --git a/public/src/client/groups/list.js b/public/src/client/groups/list.js new file mode 100644 index 0000000000..e7daf195f1 --- /dev/null +++ b/public/src/client/groups/list.js @@ -0,0 +1,26 @@ +"use strict"; +/* globals define, ajaxify, socket */ + +define('forum/groups/list', function() { + var Groups = {}; + + Groups.init = function() { + var groupsEl = $('.groups.row'); + + // Group joining and leaving + groupsEl.on('click', '[data-action]', function() { + var action = $(this).attr('data-action'), + groupName = $(this).parents('[data-group]').attr('data-group'); + + socket.emit('groups.' + action, { + groupName: groupName + }, function(err) { + if (!err) { + ajaxify.refresh(); + } + }); + }); + }; + + return Groups; +}); \ No newline at end of file diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js new file mode 100644 index 0000000000..9fd29bc2d4 --- /dev/null +++ b/src/socket.io/groups.js @@ -0,0 +1,23 @@ +"use strict"; + +var groups = require('../groups'), + + SocketGroups = {}; + +SocketGroups.join = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.join(data.groupName, socket.uid, callback); +}; + +SocketGroups.leave = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.leave(data.groupName, socket.uid, callback); +}; + +module.exports = SocketGroups; From a907fa2187a90ade503da100d40ac99746e3b43e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 8 Jan 2015 12:47:24 -0500 Subject: [PATCH 06/28] added concept of group ownership, #2588 --- public/language/en_GB/groups.json | 1 + src/groups.js | 50 +++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/public/language/en_GB/groups.json b/public/language/en_GB/groups.json index cc4aeb8997..9aade4c62d 100644 --- a/public/language/en_GB/groups.json +++ b/public/language/en_GB/groups.json @@ -1,6 +1,7 @@ { "groups": "Groups", "view_group": "View Group", + "owner": "Group Owner", "details.title": "Group Details", "details.members": "Member List", diff --git a/src/groups.js b/src/groups.js index 4f59d39820..2dcb708f66 100644 --- a/src/groups.js +++ b/src/groups.js @@ -123,7 +123,22 @@ var async = require('async'), } if (options.expand) { - async.map(uids, user.getUserData, next); + async.waterfall([ + async.apply(async.map, uids, user.getUserData), + function(users, next) { + async.mapLimit(users, 10, function(userObj, next) { + Groups.ownership.isOwner(userObj.uid, groupName, function(err, isOwner) { + if (err) { + winston.warn('[groups.get] Could not determine ownership in group `' + groupName + '` for uid `' + userObj.uid + '`: ' + err.message); + return next(null, userObj); + } + + userObj.isOwner = isOwner; + next(null, userObj); + }); + }, next); + } + ], next); } else { next(err, uids); } @@ -134,13 +149,9 @@ var async = require('async'), return callback(err); } - results.base.members = results.users.filter(function(user) { - return typeof user !== 'undefined'; - }); - + results.base.members = results.users.filter(Boolean); results.base.count = numUsers || results.base.members.length; results.base.memberCount = numUsers || results.base.members.length; - results.base.deleted = !!parseInt(results.base.deleted, 10); results.base.hidden = !!parseInt(results.base.hidden, 10); results.base.system = !!parseInt(results.base.system, 10); @@ -603,4 +614,31 @@ var async = require('async'), }); }); }; + + Groups.ownership = {}; + + Groups.ownership.isOwner = function(uid, groupName, callback) { + // Note: All admins are also owners + async.waterfall([ + async.apply(db.isSetMember, 'group:' + groupName + ':owners', uid), + function(isOwner, next) { + if (isOwner) { + return next(null, isOwner); + } + + user.isAdministrator(uid, next); + } + ], callback); + }; + + Groups.ownership.grant = function(toUid, groupName, callback) { + // Note: No ownership checking is done here on purpose! + db.setAdd('group:' + groupName + ':owners', toUid, callback); + }; + + Groups.ownership.rescind = function(toUid, groupName, callback) { + // Note: No ownership checking is done here on purpose! + db.setRemove('group:' + groupName + ':owners', toUid, callback); + }; + }(module.exports)); From d747ce5760a7d0a8ad00a46a9b951d88a99c7327 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 8 Jan 2015 15:13:05 -0500 Subject: [PATCH 07/28] granting and rescinding ownership, #2588 --- public/src/client/groups/details.js | 30 ++++++++++++---- src/controllers/groups.js | 4 ++- src/groups.js | 55 +++++++++++++++++++---------- src/socket.io/groups.js | 28 +++++++++++++++ 4 files changed, 91 insertions(+), 26 deletions(-) diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 12ca74884a..e8d4ed957f 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -1,17 +1,35 @@ "use strict"; +/* globals socket, ajaxify */ define('forum/groups/details', function() { var Details = {}; Details.init = function() { - var memberListEl = $('.groups.details .members'); - - memberListEl.on('click', '[data-slug]', function() { - var slug = this.getAttribute('data-slug'); - ajaxify.go('user/' + slug); - }); + var memberList = $('.groups .members'); $('.latest-posts .content img').addClass('img-responsive'); + + memberList.on('click', '[data-action]', function() { + var btnEl = $(this), + userRow = btnEl.parents('tr'), + ownerFlagEl = userRow.find('.member-name i'), + isOwner = !ownerFlagEl.hasClass('hidden') ? true : false, + uid = userRow.attr('data-uid'), + action = btnEl.attr('data-action'); + + switch(action) { + case 'toggleOwnership': + socket.emit('groups.' + (isOwner ? 'rescind' : 'grant'), { + toUid: uid, + groupName: ajaxify.variables.get('group_name') + }, function(err) { + if (!err) { + ownerFlagEl.toggleClass('hidden'); + } + }); + break; + } + }); }; return Details; diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 72d83d4fc2..2d2ce8f050 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -23,10 +23,12 @@ groupsController.list = function(req, res, next) { groupsController.details = function(req, res, next) { var uid = req.user ? parseInt(req.user.uid, 10) : 0; + async.parallel({ group: function(next) { groups.get(req.params.name, { - expand: true + expand: true, + uid: uid }, next); }, posts: function(next) { diff --git a/src/groups.js b/src/groups.js index 2dcb708f66..60019a47b9 100644 --- a/src/groups.js +++ b/src/groups.js @@ -71,25 +71,7 @@ var async = require('async'), groupNames = groupNames.concat(ephemeralGroups); async.map(groupNames, function (groupName, next) { - async.waterfall([ - async.apply(Groups.get, groupName, options), - function(groupObj, next) { - // Retrieve group membership state, if uid is passed in - if (!options.uid) { - return next(null, groupObj); - } - - Groups.isMember(options.uid, groupName, function(err, isMember) { - if (err) { - winston.warn('[groups.list] Could not determine membership in group `' + groupName + '` for uid `' + options.uid + '`: ' + err.message); - return next(null, groupObj); - } - - groupObj.isMember = isMember; - next(null, groupObj); - }); - } - ], next); + Groups.get(groupName, options, next); }, function (err, groups) { callback(err, internals.filterGroups(groups, options)); }); @@ -126,6 +108,9 @@ var async = require('async'), async.waterfall([ async.apply(async.map, uids, user.getUserData), function(users, next) { + // Filter out non-matches + users = users.filter(Boolean); + async.mapLimit(users, 10, function(userObj, next) { Groups.ownership.isOwner(userObj.uid, groupName, function(err, isOwner) { if (err) { @@ -143,6 +128,36 @@ var async = require('async'), next(err, uids); } }); + }, + isMember: function(next) { + // Retrieve group membership state, if uid is passed in + if (!options.uid) { + return next(); + } + + Groups.isMember(options.uid, groupName, function(err, isMember) { + if (err) { + winston.warn('[groups.get] Could not determine membership in group `' + groupName + '` for uid `' + options.uid + '`: ' + err.message); + return next(); + } + + next(null, isMember); + }); + }, + isOwner: function(next) { + // Retrieve group ownership state, if uid is passed in + if (!options.uid) { + return next(); + } + + Groups.ownership.isOwner(options.uid, groupName, function(err, isOwner) { + if (err) { + winston.warn('[groups.get] Could not determine ownership in group `' + groupName + '` for uid `' + options.uid + '`: ' + err.message); + return next(); + } + + next(null, isOwner); + }); } }, function (err, results) { if (err || !results.base) { @@ -157,6 +172,8 @@ var async = require('async'), results.base.system = !!parseInt(results.base.system, 10); results.base.deletable = !results.base.system; results.base.truncated = truncated; + results.base.isMember = results.isMember; + results.base.isOwner = results.isOwner; callback(err, results.base); }); diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 9fd29bc2d4..943b9dc10c 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -20,4 +20,32 @@ SocketGroups.leave = function(socket, data, callback) { groups.leave(data.groupName, socket.uid, callback); }; +SocketGroups.grant = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + if (!isOwner) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.ownership.grant(data.toUid, data.groupName, callback); + }); +}; + +SocketGroups.rescind = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + if (!isOwner) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.ownership.rescind(data.toUid, data.groupName, callback); + }); +}; + module.exports = SocketGroups; From bb0b6d9db451a97b0c8d3165e28c9e7401caf9df Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 8 Jan 2015 15:36:34 -0500 Subject: [PATCH 08/28] using 'invisible' class instead of 'hidden', #2588 --- public/src/client/groups/details.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index e8d4ed957f..af3bf54afa 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -13,7 +13,7 @@ define('forum/groups/details', function() { var btnEl = $(this), userRow = btnEl.parents('tr'), ownerFlagEl = userRow.find('.member-name i'), - isOwner = !ownerFlagEl.hasClass('hidden') ? true : false, + isOwner = !ownerFlagEl.hasClass('invisible') ? true : false, uid = userRow.attr('data-uid'), action = btnEl.attr('data-action'); @@ -24,7 +24,7 @@ define('forum/groups/details', function() { groupName: ajaxify.variables.get('group_name') }, function(err) { if (!err) { - ownerFlagEl.toggleClass('hidden'); + ownerFlagEl.toggleClass('invisible'); } }); break; From 71b41239ac5fa0de762060584d4e2300f2fd8062 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 8 Jan 2015 16:50:31 -0500 Subject: [PATCH 09/28] attempting to join a private group causes the join to be pending, #2588 --- public/src/modules/helpers.js | 2 +- src/groups.js | 29 +++++++++++++++++++++++++++++ src/socket.io/groups.js | 13 ++++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index 838ad7a4da..bc40bde658 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -26,7 +26,7 @@ if (groupObj.isMember) { return ''; } else { - if (groupObj.pending) { + if (groupObj.isPending) { return ''; } else { return ''; diff --git a/src/groups.js b/src/groups.js index 60019a47b9..eadabda28f 100644 --- a/src/groups.js +++ b/src/groups.js @@ -144,6 +144,14 @@ var async = require('async'), next(null, isMember); }); }, + isPending: function(next) { + // Retrieve group membership state, if uid is passed in + if (!options.uid) { + return next(); + } + + db.isSetMember('group:' + groupName + ':pending', options.uid, next); + }, isOwner: function(next) { // Retrieve group ownership state, if uid is passed in if (!options.uid) { @@ -173,12 +181,19 @@ var async = require('async'), results.base.deletable = !results.base.system; results.base.truncated = truncated; results.base.isMember = results.isMember; + results.base.isPending = results.isPending; results.base.isOwner = results.isOwner; callback(err, results.base); }); }; + Groups.isPrivate = function(groupName, callback) { + db.getObjectField('group:' + groupName, 'private', function(err, isPrivate) { + callback(err, isPrivate || isPrivate === null); // Private, if not set at all + }); + }; + Groups.getMembers = function(groupName, callback) { db.getSetMembers('group:' + groupName + ':members', callback); }; @@ -521,6 +536,20 @@ var async = require('async'), }); }; + Groups.requestMembership = function(groupName, uid, callback) { + db.setAdd('group:' + groupName + ':pending', uid, callback); + plugins.fireHook('action:groups.requestMembership', { + groupName: groupName, + uid: uid + }); + }; + + Groups.approveMembership = function(groupName, uid, callback) { + // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! + db.setRemove('group:' + groupName + ':pending', uid, callback); + Groups.join.apply(Groups, arguments); + }; + Groups.leave = function(groupName, uid, callback) { callback = callback || function() {}; diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 943b9dc10c..366a12348f 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -1,6 +1,7 @@ "use strict"; var groups = require('../groups'), + meta = require('../meta'), SocketGroups = {}; @@ -9,7 +10,17 @@ SocketGroups.join = function(socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - groups.join(data.groupName, socket.uid, callback); + if (meta.config.allowPrivateGroups) { + groups.isPrivate(data.groupName, function(err, isPrivate) { + if (isPrivate) { + groups.requestMembership(data.groupName, socket.uid, callback); + } else { + groups.join(data.groupName, socket.uid, callback); + } + }); + } else { + groups.join(data.groupName, socket.uid, callback); + } }; SocketGroups.leave = function(socket, data, callback) { From 3712fe1fb5bac4200e5a5e0f97fe5bacc35176ce Mon Sep 17 00:00:00 2001 From: psychobunny Date: Thu, 8 Jan 2015 16:57:15 -0500 Subject: [PATCH 10/28] latest t.js --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b449297adc..1efe0580c4 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "socket.io-redis": "^0.1.3", "socketio-wildcard": "~0.1.1", "string": "^3.0.0", - "templates.js": "0.1.9", + "templates.js": "0.1.10", "uglify-js": "git+https://github.com/julianlam/UglifyJS2.git", "underscore": "~1.7.0", "validator": "~3.26.0", From 3a3d45155e76883b1d8aa29cb5f70cd0350d73b8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 8 Jan 2015 17:06:33 -0500 Subject: [PATCH 11/28] show pending members in frontend UI, #2588 --- public/language/en_GB/groups.json | 1 + src/groups.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/public/language/en_GB/groups.json b/public/language/en_GB/groups.json index 9aade4c62d..d537e28667 100644 --- a/public/language/en_GB/groups.json +++ b/public/language/en_GB/groups.json @@ -5,6 +5,7 @@ "details.title": "Group Details", "details.members": "Member List", + "details.pending": "Pending Members", "details.has_no_posts": "This group's members have not made any posts.", "details.latest_posts": "Latest Posts" } \ No newline at end of file diff --git a/src/groups.js b/src/groups.js index eadabda28f..f6f619ff87 100644 --- a/src/groups.js +++ b/src/groups.js @@ -129,6 +129,19 @@ var async = require('async'), } }); }, + pending: function (next) { + db.getSetMembers('group:' + groupName + ':pending', function (err, uids) { + if (err) { + return next(err); + } + + if (options.expand) { + async.map(uids, user.getUserData, next); + } else { + next(err, uids); + } + }); + }, isMember: function(next) { // Retrieve group membership state, if uid is passed in if (!options.uid) { @@ -173,6 +186,7 @@ var async = require('async'), } results.base.members = results.users.filter(Boolean); + results.base.pending = results.pending.filter(Boolean); results.base.count = numUsers || results.base.members.length; results.base.memberCount = numUsers || results.base.members.length; results.base.deleted = !!parseInt(results.base.deleted, 10); From 96c37c25b05e485f3852a67a329b57234dc0313c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 8 Jan 2015 17:23:40 -0500 Subject: [PATCH 12/28] join, leave, accept, reject: handlers + UI, #2588 --- public/src/client/groups/details.js | 24 +++++++++++++++++++++--- src/groups.js | 7 ++++++- src/socket.io/groups.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index af3bf54afa..15291cb9d8 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -1,15 +1,15 @@ "use strict"; -/* globals socket, ajaxify */ +/* globals define, socket, ajaxify, app */ define('forum/groups/details', function() { var Details = {}; Details.init = function() { - var memberList = $('.groups .members'); + var detailsPage = $('.groups'); $('.latest-posts .content img').addClass('img-responsive'); - memberList.on('click', '[data-action]', function() { + detailsPage.on('click', '[data-action]', function() { var btnEl = $(this), userRow = btnEl.parents('tr'), ownerFlagEl = userRow.find('.member-name i'), @@ -25,6 +25,24 @@ define('forum/groups/details', function() { }, function(err) { if (!err) { ownerFlagEl.toggleClass('invisible'); + } else { + app.alertError(err); + } + }); + break; + + case 'join': // intentional fall-throughs! + case 'leave': + case 'accept': + case 'reject': + socket.emit('groups.' + action, { + toUid: uid, + groupName: ajaxify.variables.get('group_name') + }, function(err) { + if (!err) { + ajaxify.refresh(); + } else { + app.alertError(err); } }); break; diff --git a/src/groups.js b/src/groups.js index f6f619ff87..6286fe7a9d 100644 --- a/src/groups.js +++ b/src/groups.js @@ -558,12 +558,17 @@ var async = require('async'), }); }; - Groups.approveMembership = function(groupName, uid, callback) { + Groups.acceptMembership = function(groupName, uid, callback) { // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! db.setRemove('group:' + groupName + ':pending', uid, callback); Groups.join.apply(Groups, arguments); }; + Groups.rejectMembership = function(groupName, uid, callback) { + // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! + db.setRemove('group:' + groupName + ':pending', uid, callback); + }; + Groups.leave = function(groupName, uid, callback) { callback = callback || function() {}; diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 366a12348f..039ad42b89 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -59,4 +59,32 @@ SocketGroups.rescind = function(socket, data, callback) { }); }; +SocketGroups.accept = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + if (!isOwner) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.acceptMembership(data.groupName, data.toUid, callback); + }); +}; + +SocketGroups.reject = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + if (!isOwner) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.rejectMembership(data.groupName, data.toUid, callback); + }); +}; + module.exports = SocketGroups; From 65537d997f4155a483638cd15716caabdc37aca9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 9 Jan 2015 09:43:45 -0500 Subject: [PATCH 13/28] if a group is deleted, flush the owner and pending sets, too --- src/groups.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/groups.js b/src/groups.js index 6286fe7a9d..f8d5470035 100644 --- a/src/groups.js +++ b/src/groups.js @@ -500,15 +500,11 @@ var async = require('async'), Groups.destroy = function(groupName, callback) { async.parallel([ - function(next) { - db.delete('group:' + groupName, next); - }, - function(next) { - db.setRemove('groups', groupName, next); - }, - function(next) { - db.delete('group:' + groupName + ':members', next); - }, + async.apply(db.delete, 'group:' + groupName), + async.apply(db.setRemove, 'groups', groupName), + async.apply(db.delete, 'group:' + groupName + ':members'), + async.apply(db.delete, 'group:' + groupName + ':pending'), + async.apply(db.delete, 'group:' + groupName + ':owners'), function(next) { db.getSetMembers('groups', function(err, groups) { if (err) { From 84c19921b99ac93be592671cf21ea0b178f9e823 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 9 Jan 2015 10:33:54 -0500 Subject: [PATCH 14/28] newly created groups automatically assign the creator as the owner, some tweaks to isPrivate parsing, added groups link to header, #2588 --- public/language/en_GB/global.json | 1 + public/src/admin/manage/groups.js | 4 +-- src/groups.js | 56 ++++++++++++++++++------------- src/socket.io/admin/groups.js | 8 +++-- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/public/language/en_GB/global.json b/public/language/en_GB/global.json index a373c7a4d3..240b11d3a0 100644 --- a/public/language/en_GB/global.json +++ b/public/language/en_GB/global.json @@ -33,6 +33,7 @@ "header.tags": "Tags", "header.popular": "Popular", "header.users": "Users", + "header.groups": "Groups", "header.chats": "Chats", "header.notifications": "Notifications", "header.search": "Search", diff --git a/public/src/admin/manage/groups.js b/public/src/admin/manage/groups.js index 61e1e3b0f8..8f4b341f7d 100644 --- a/public/src/admin/manage/groups.js +++ b/public/src/admin/manage/groups.js @@ -49,7 +49,7 @@ define('admin/manage/groups', [ }, errorText; - socket.emit('admin.groups.create', submitObj, function(err, data) { + socket.emit('admin.groups.create', submitObj, function(err) { if (err) { switch (err) { case 'group-exists': @@ -68,7 +68,7 @@ define('admin/manage/groups', [ createModalError.addClass('hide'); createGroupName.val(''); createModal.on('hidden.bs.modal', function() { - ajaxify.go('admin/manage/groups'); + ajaxify.refresh(); }); createModal.modal('hide'); } diff --git a/src/groups.js b/src/groups.js index f8d5470035..e9e8685d6d 100644 --- a/src/groups.js +++ b/src/groups.js @@ -204,7 +204,13 @@ var async = require('async'), Groups.isPrivate = function(groupName, callback) { db.getObjectField('group:' + groupName, 'private', function(err, isPrivate) { - callback(err, isPrivate || isPrivate === null); // Private, if not set at all + isPrivate = isPrivate || isPrivate === null; + + if (typeof isPrivate === 'string') { + isPrivate = (isPrivate === '0' ? false : true); + } + + callback(err, isPrivate); // Private, if not set at all }); }; @@ -358,16 +364,16 @@ var async = require('async'), } }; - Groups.create = function(name, description, callback) { - if (name.length === 0) { + Groups.create = function(data, callback) { + if (data.name.length === 0) { return callback(new Error('[[error:group-name-too-short]]')); } - if (name === 'administrators' || name === 'registered-users') { + if (data.name === 'administrators' || data.name === 'registered-users') { var system = true; } - meta.userOrGroupExists(name, function (err, exists) { + meta.userOrGroupExists(data.name, function (err, exists) { if (err) { return callback(err); } @@ -377,24 +383,25 @@ var async = require('async'), } var groupData = { - name: name, - userTitle: name, - description: description, - deleted: '0', - hidden: '0', - system: system ? '1' : '0' - }; - - async.parallel([ - function(next) { - db.setAdd('groups', name, next); + name: data.name, + userTitle: data.name, + description: data.description, + deleted: '0', + hidden: '0', + system: system ? '1' : '0', + 'private': data.private || '1' }, - function(next) { - db.setObject('group:' + name, groupData, function(err) { - Groups.get(name, {}, next); - }); - } - ], callback); + tasks = [ + async.apply(db.setAdd, 'groups', data.name), + async.apply(db.setObject, 'group:' + data.name, groupData) + ]; + + if (data.hasOwnProperty('ownerUid')) { + tasks.push(async.apply(db.setAdd, 'group:' + data.name + ':owners', data.ownerUid)); + tasks.push(async.apply(db.setAdd, 'group:' + data.name + ':members', data.ownerUid)); + } + + async.parallel(tasks, callback); }); }; @@ -529,7 +536,10 @@ var async = require('async'), uid: uid }); } else { - Groups.create(groupName, '', function(err) { + Groups.create({ + name: groupName, + description: '' + }, function(err) { if (err && err.message !== '[[error:group-already-exists]]') { winston.error('[groups.join] Could not create new hidden group: ' + err.message); return callback(err); diff --git a/src/socket.io/admin/groups.js b/src/socket.io/admin/groups.js index 9aec355986..0bcf60ab7b 100644 --- a/src/socket.io/admin/groups.js +++ b/src/socket.io/admin/groups.js @@ -8,9 +8,11 @@ Groups.create = function(socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - groups.create(data.name, data.description, function(err, groupObj) { - callback(err, groupObj || undefined); - }); + groups.create({ + name: data.name, + description: data.description, + ownerUid: socket.uid + }, callback); }; Groups.delete = function(socket, groupName, callback) { From 7be08307514f9362d04deb850384a631cdf0fa2d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 9 Jan 2015 10:45:50 -0500 Subject: [PATCH 15/28] properly showing private state in group details, #2588 --- public/language/en_GB/groups.json | 4 +++- src/groups.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/language/en_GB/groups.json b/public/language/en_GB/groups.json index d537e28667..8abfdbec4e 100644 --- a/public/language/en_GB/groups.json +++ b/public/language/en_GB/groups.json @@ -7,5 +7,7 @@ "details.members": "Member List", "details.pending": "Pending Members", "details.has_no_posts": "This group's members have not made any posts.", - "details.latest_posts": "Latest Posts" + "details.latest_posts": "Latest Posts", + "details.private": "Private Group", + "details.public": "Public Group" } \ No newline at end of file diff --git a/src/groups.js b/src/groups.js index e9e8685d6d..50b5bc5c8d 100644 --- a/src/groups.js +++ b/src/groups.js @@ -192,6 +192,7 @@ var async = require('async'), results.base.deleted = !!parseInt(results.base.deleted, 10); results.base.hidden = !!parseInt(results.base.hidden, 10); results.base.system = !!parseInt(results.base.system, 10); + results.base.private = results.base.private ? !!parseInt(results.base.private, 10) : true; results.base.deletable = !results.base.system; results.base.truncated = truncated; results.base.isMember = results.isMember; From b4731ad47bf3db9ed12c879decf3dac185e4d7b2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 9 Jan 2015 13:51:27 -0500 Subject: [PATCH 16/28] added colorpicker to frontend, badge colour changes for groups, #2588 --- public/language/en_GB/groups.json | 4 +- public/src/client/groups/details.js | 68 ++++++++++++++++++++++- public/vendor/colorpicker/colorpicker.css | 24 ++++---- src/groups.js | 3 +- src/meta/css.js | 4 +- src/socket.io/admin/groups.js | 1 + src/socket.io/groups.js | 14 +++++ src/views/admin/header.tpl | 1 - 8 files changed, 102 insertions(+), 17 deletions(-) diff --git a/public/language/en_GB/groups.json b/public/language/en_GB/groups.json index 8abfdbec4e..1dafb41ed6 100644 --- a/public/language/en_GB/groups.json +++ b/public/language/en_GB/groups.json @@ -9,5 +9,7 @@ "details.has_no_posts": "This group's members have not made any posts.", "details.latest_posts": "Latest Posts", "details.private": "Private Group", - "details.public": "Public Group" + "details.public": "Public Group", + + "details.owner_options": "Group Administration" } \ No newline at end of file diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 15291cb9d8..1d6cb4141c 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -5,7 +5,10 @@ define('forum/groups/details', function() { var Details = {}; Details.init = function() { - var detailsPage = $('.groups'); + var detailsPage = $('.groups'), + settingsFormEl = detailsPage.find('form'); + + Details.prepareSettings(); $('.latest-posts .content img').addClass('img-responsive'); @@ -31,6 +34,10 @@ define('forum/groups/details', function() { }); break; + case 'update': + Details.update(); + break; + case 'join': // intentional fall-throughs! case 'leave': case 'accept': @@ -50,5 +57,64 @@ define('forum/groups/details', function() { }); }; + Details.prepareSettings = function() { + var settingsFormEl = $('.groups form'), + colorBtn = settingsFormEl.find('[data-action="color-select"]'), + colorValueEl = settingsFormEl.find('[name="labelColor"]'), + previewEl = settingsFormEl.find('.label'); + + if (settingsFormEl.length) { + // Add color picker to settings form + require(['vendor/colorpicker/colorpicker'], function() { + console.log(colorBtn, colorBtn.ColorPicker); + colorBtn.ColorPicker({ + color: colorValueEl.val() || '#000', + onChange: function(hsb, hex) { + colorValueEl.val('#' + hex); + previewEl.css('background-color', '#' + hex); + }, + onShow: function(colpkr) { + $(colpkr).css('z-index', 1051); + } + }); + }); + } + }; + + Details.update = function() { + var settingsFormEl = $('.groups form'); + + if (settingsFormEl.length) { + require(['vendor/jquery/serializeObject/jquery.ba-serializeobject.min'], function() { + var settings = settingsFormEl.serializeObject(), + keys = Object.keys(settings), + inputEl; + + // Fix checkbox values + keys.forEach(function(key) { + inputEl = settingsFormEl.find('input[type="checkbox"][name="' + key + '"]'); + if (inputEl.length) { + settings[key] = settings[key] === 'on' ? true : false; + } + }); + + socket.emit('groups.update', { + groupName: ajaxify.variables.get('group_name'), + values: settings + }, function(err) { + if (err) { + return app.alertError(err.message); + } + + if (settings.name) { + ajaxify.go('groups/' + encodeURIComponent(settings.name)); + } else { + ajaxify.refresh(); + } + }); + }); + } + }; + return Details; }); \ No newline at end of file diff --git a/public/vendor/colorpicker/colorpicker.css b/public/vendor/colorpicker/colorpicker.css index b5b2158534..5ee96ba682 100644 --- a/public/vendor/colorpicker/colorpicker.css +++ b/public/vendor/colorpicker/colorpicker.css @@ -3,7 +3,7 @@ height: 176px; overflow: hidden; position: absolute; - background: url(./images/custom_background.png); + background: url(./vendor/colorpicker/images/custom_background.png); font-family: Arial, Helvetica, sans-serif; display: none; } @@ -23,7 +23,7 @@ left: 0; width: 150px; height: 150px; - background: url(./images/colorpicker_overlay.png); + background: url(./vendor/colorpicker/images/colorpicker_overlay.png); } .colorpicker_color div div { position: absolute; @@ -32,7 +32,7 @@ width: 11px; height: 11px; overflow: hidden; - background: url(./images/colorpicker_select.gif); + background: url(./vendor/colorpicker/images/colorpicker_select.gif); margin: -5px 0 0 -5px; } .colorpicker_hue { @@ -48,7 +48,7 @@ width: 35px; height: 9px; overflow: hidden; - background: url(./images/custom_indic.gif) left top; + background: url(./vendor/colorpicker/images/custom_indic.gif) left top; margin: -4px 0 0 0; left: 0px; } @@ -86,7 +86,7 @@ position: absolute; width: 72px; height: 22px; - background: url(./images/custom_hex.png) top; + background: url(./vendor/colorpicker/images/custom_hex.png) top; left: 212px; top: 142px; } @@ -109,32 +109,32 @@ cursor: n-resize; } .colorpicker_rgb_r { - background-image: url(./images/custom_rgb_r.png); + background-image: url(./vendor/colorpicker/images/custom_rgb_r.png); top: 52px; left: 212px; } .colorpicker_rgb_g { - background-image: url(./images/custom_rgb_g.png); + background-image: url(./vendor/colorpicker/images/custom_rgb_g.png); top: 82px; left: 212px; } .colorpicker_rgb_b { - background-image: url(./images/custom_rgb_b.png); + background-image: url(./vendor/colorpicker/images/custom_rgb_b.png); top: 112px; left: 212px; } .colorpicker_hsb_h { - background-image: url(./images/custom_hsb_h.png); + background-image: url(./vendor/colorpicker/images/custom_hsb_h.png); top: 52px; left: 282px; } .colorpicker_hsb_s { - background-image: url(./images/custom_hsb_s.png); + background-image: url(./vendor/colorpicker/images/custom_hsb_s.png); top: 82px; left: 282px; } .colorpicker_hsb_b { - background-image: url(./images/custom_hsb_b.png); + background-image: url(./vendor/colorpicker/images/custom_hsb_b.png); top: 112px; left: 282px; } @@ -143,7 +143,7 @@ position: absolute; width: 22px; height: 22px; - background: url(./images/custom_submit.png) top; + background: url(./vendor/colorpicker/images/custom_submit.png) top; left: 322px; top: 142px; overflow: hidden; diff --git a/src/groups.js b/src/groups.js index 50b5bc5c8d..333b823396 100644 --- a/src/groups.js +++ b/src/groups.js @@ -423,7 +423,8 @@ var async = require('async'), description: values.description || '', icon: values.icon || '', labelColor: values.labelColor || '#000000', - hidden: values.hidden || '0' + hidden: values.hidden || '0', + 'private': values.private === false ? '0' : '1' }, function(err) { if (err) { return callback(err); diff --git a/src/meta/css.js b/src/meta/css.js index 5021e9b302..9c00ba8633 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -49,9 +49,11 @@ module.exports = function(Meta) { source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css";'; source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";'; - + source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";'; acpSource = '\n@import "..' + path.sep + 'public/less/admin/admin";\n' + source; + acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";'; + source = '@import "./theme";\n' + source; async.parallel([ diff --git a/src/socket.io/admin/groups.js b/src/socket.io/admin/groups.js index 0bcf60ab7b..b78380428e 100644 --- a/src/socket.io/admin/groups.js +++ b/src/socket.io/admin/groups.js @@ -41,6 +41,7 @@ Groups.leave = function(socket, data, callback) { groups.leave(data.groupName, data.uid, callback); }; +// Possibly remove this and call SocketGroups.update instead Groups.update = function(socket, data, callback) { if(!data) { return callback(new Error('[[error:invalid-data]]')); diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 039ad42b89..fe401a4d15 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -87,4 +87,18 @@ SocketGroups.reject = function(socket, data, callback) { }); }; +SocketGroups.update = function(socket, data, callback) { + if(!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + if (!isOwner) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.update(data.groupName, data.values, callback); + }); +}; + module.exports = SocketGroups; diff --git a/src/views/admin/header.tpl b/src/views/admin/header.tpl index dfa2dc9c89..9bd680936a 100644 --- a/src/views/admin/header.tpl +++ b/src/views/admin/header.tpl @@ -5,7 +5,6 @@ - From 311d02dbe1eea00595e5e18c04b3942d114a3a88 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 9 Jan 2015 13:53:19 -0500 Subject: [PATCH 17/28] removed console log --- public/src/client/groups/details.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 1d6cb4141c..f4e889d70f 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -66,7 +66,6 @@ define('forum/groups/details', function() { if (settingsFormEl.length) { // Add color picker to settings form require(['vendor/colorpicker/colorpicker'], function() { - console.log(colorBtn, colorBtn.ColorPicker); colorBtn.ColorPicker({ color: colorValueEl.val() || '#000', onChange: function(hsb, hex) { From 69ed8ac45d5f5549ac5fc5796b4b8df6b041fe3b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sun, 11 Jan 2015 17:24:15 -0500 Subject: [PATCH 18/28] icon selection interface -- moved iconSelect module out of admin space, #2588 --- public/less/admin/admin.less | 19 ------------------- public/src/admin/manage/categories.js | 2 +- public/src/admin/manage/groups.js | 2 +- public/src/client/groups/details.js | 11 +++++++++-- public/src/{admin => }/modules/iconSelect.js | 6 ++++-- src/views/admin/manage/categories.tpl | 2 +- src/views/admin/manage/groups.tpl | 2 +- src/views/admin/partials/fontawesome.tpl | 1 - 8 files changed, 17 insertions(+), 28 deletions(-) rename public/src/{admin => }/modules/iconSelect.js (83%) delete mode 100644 src/views/admin/partials/fontawesome.tpl diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less index 2e5b868048..4042e3d621 100644 --- a/public/less/admin/admin.less +++ b/public/less/admin/admin.less @@ -195,25 +195,6 @@ } } - .icon-container { - .row { - margin: 0; - i { - width:20px; - height:20px; - margin: 1px; - .pointer; - line-height: 20px; - text-align: center; - - &:hover, &.selected { - background: black; - color: white; - } - } - } - } - .navbar { padding: 0 5px; diff --git a/public/src/admin/manage/categories.js b/public/src/admin/manage/categories.js index 5bf1bfe945..54385b6d0c 100644 --- a/public/src/admin/manage/categories.js +++ b/public/src/admin/manage/categories.js @@ -3,7 +3,7 @@ define('admin/manage/categories', [ 'uploader', - 'admin/modules/iconSelect', + 'iconSelect', 'admin/modules/colorpicker' ], function(uploader, iconSelect, colorpicker) { var Categories = {}; diff --git a/public/src/admin/manage/groups.js b/public/src/admin/manage/groups.js index 8f4b341f7d..19f0bf31e9 100644 --- a/public/src/admin/manage/groups.js +++ b/public/src/admin/manage/groups.js @@ -2,7 +2,7 @@ /*global define, templates, socket, ajaxify, app, admin, bootbox*/ define('admin/manage/groups', [ - 'admin/modules/iconSelect', + 'iconSelect', 'admin/modules/colorpicker' ], function(iconSelect, colorpicker) { var Groups = {}; diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index f4e889d70f..0c46f1580a 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -1,7 +1,7 @@ "use strict"; /* globals define, socket, ajaxify, app */ -define('forum/groups/details', function() { +define('forum/groups/details', ['iconSelect'], function(iconSelect) { var Details = {}; Details.init = function() { @@ -61,7 +61,9 @@ define('forum/groups/details', function() { var settingsFormEl = $('.groups form'), colorBtn = settingsFormEl.find('[data-action="color-select"]'), colorValueEl = settingsFormEl.find('[name="labelColor"]'), - previewEl = settingsFormEl.find('.label'); + iconBtn = settingsFormEl.find('[data-action="icon-select"]'), + previewEl = settingsFormEl.find('.label'), + previewIcon = previewEl.find('i'); if (settingsFormEl.length) { // Add color picker to settings form @@ -77,6 +79,11 @@ define('forum/groups/details', function() { } }); }); + + // Add icon selection interface + iconSelect.init(previewIcon, function() { + console.log(arguments); + }); } }; diff --git a/public/src/admin/modules/iconSelect.js b/public/src/modules/iconSelect.js similarity index 83% rename from public/src/admin/modules/iconSelect.js rename to public/src/modules/iconSelect.js index 286009bf0f..88ab1903ed 100644 --- a/public/src/admin/modules/iconSelect.js +++ b/public/src/modules/iconSelect.js @@ -7,7 +7,9 @@ define(function() { iconSelect.init = function(el, onModified) { onModified = onModified || function() {}; - var selected = el.attr('class').replace('fa-2x', '').replace('fa', '').replace(/\s+/g, ''); + var doubleSize = el.hasClass('fa-2x'), + selected = el.attr('class').replace('fa-2x', '').replace('fa', '').replace(/\s+/g, ''); + $('#icons .selected').removeClass('selected'); if (selected === '') { @@ -25,7 +27,7 @@ define(function() { categoryIconClass = ''; } - el.attr('class', 'fa fa-2x ' + categoryIconClass); + el.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : '') + categoryIconClass); el.val(categoryIconClass); el.attr('value', categoryIconClass); diff --git a/src/views/admin/manage/categories.tpl b/src/views/admin/manage/categories.tpl index b3b9427abb..2c0dcf48da 100644 --- a/src/views/admin/manage/categories.tpl +++ b/src/views/admin/manage/categories.tpl @@ -149,7 +149,7 @@
    - +
    diff --git a/src/views/admin/manage/groups.tpl b/src/views/admin/manage/groups.tpl index ee8f8f24ea..69f8bad37b 100644 --- a/src/views/admin/manage/groups.tpl +++ b/src/views/admin/manage/groups.tpl @@ -136,7 +136,7 @@
    - +
    diff --git a/src/views/admin/partials/fontawesome.tpl b/src/views/admin/partials/fontawesome.tpl deleted file mode 100644 index b0263d95a9..0000000000 --- a/src/views/admin/partials/fontawesome.tpl +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 240056d1694a7e271ebdb30d2bcb17f946f8188f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sun, 11 Jan 2015 17:44:51 -0500 Subject: [PATCH 19/28] added icon container styles back to ACP, proper hooking-up of iconSelect module in group settings panel, #2588 --- public/less/admin/admin.less | 19 ++++++++++++++++++ public/src/client/groups/details.js | 31 +++++++++++++++-------------- src/socket.io/groups.js | 2 +- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less index 4042e3d621..2e5b868048 100644 --- a/public/less/admin/admin.less +++ b/public/less/admin/admin.less @@ -195,6 +195,25 @@ } } + .icon-container { + .row { + margin: 0; + i { + width:20px; + height:20px; + margin: 1px; + .pointer; + line-height: 20px; + text-align: center; + + &:hover, &.selected { + background: black; + color: white; + } + } + } + } + .navbar { padding: 0 5px; diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 0c46f1580a..0aacde97ce 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -1,7 +1,7 @@ "use strict"; /* globals define, socket, ajaxify, app */ -define('forum/groups/details', ['iconSelect'], function(iconSelect) { +define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker'], function(iconSelect) { var Details = {}; Details.init = function() { @@ -63,26 +63,27 @@ define('forum/groups/details', ['iconSelect'], function(iconSelect) { colorValueEl = settingsFormEl.find('[name="labelColor"]'), iconBtn = settingsFormEl.find('[data-action="icon-select"]'), previewEl = settingsFormEl.find('.label'), - previewIcon = previewEl.find('i'); + previewIcon = previewEl.find('i'), + previewValueEl = settingsFormEl.find('[name="icon"]'); if (settingsFormEl.length) { // Add color picker to settings form - require(['vendor/colorpicker/colorpicker'], function() { - colorBtn.ColorPicker({ - color: colorValueEl.val() || '#000', - onChange: function(hsb, hex) { - colorValueEl.val('#' + hex); - previewEl.css('background-color', '#' + hex); - }, - onShow: function(colpkr) { - $(colpkr).css('z-index', 1051); - } - }); + colorBtn.ColorPicker({ + color: colorValueEl.val() || '#000', + onChange: function(hsb, hex) { + colorValueEl.val('#' + hex); + previewEl.css('background-color', '#' + hex); + }, + onShow: function(colpkr) { + $(colpkr).css('z-index', 1051); + } }); // Add icon selection interface - iconSelect.init(previewIcon, function() { - console.log(arguments); + iconBtn.on('click', function() { + iconSelect.init(previewIcon, function() { + previewValueEl.val(previewIcon.val()); + }); }); } }; diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index fe401a4d15..3c9dc9b069 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -10,7 +10,7 @@ SocketGroups.join = function(socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - if (meta.config.allowPrivateGroups) { + if (meta.config.allowPrivateGroups !== '0') { groups.isPrivate(data.groupName, function(err, isPrivate) { if (isPrivate) { groups.requestMembership(data.groupName, socket.uid, callback); From 08e883de44f1523c51dfa5e77fdd23e102534d5e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sun, 11 Jan 2015 18:04:35 -0500 Subject: [PATCH 20/28] added widget areas to the bottom of the group details page, #2588 --- src/controllers/admin.js | 3 +++ src/views/admin/extend/widgets.tpl | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 5a43e1ba6c..5ccfa3a6df 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -237,6 +237,9 @@ adminController.extend.widgets = function(req, res, next) { { name: 'Global Sidebar', template: 'global', location: 'sidebar' }, { name: 'Global Header', template: 'global', location: 'header' }, { name: 'Global Footer', template: 'global', location: 'footer' }, + + { name: 'Group Page (Left)', template: 'groups/details.tpl', location: 'left'}, + { name: 'Group Page (Right)', template: 'groups/details.tpl', location: 'right'} ]; plugins.fireHook('filter:widgets.getAreas', defaultAreas, next); diff --git a/src/views/admin/extend/widgets.tpl b/src/views/admin/extend/widgets.tpl index 4571f70d1f..1f504fbdf1 100644 --- a/src/views/admin/extend/widgets.tpl +++ b/src/views/admin/extend/widgets.tpl @@ -10,7 +10,7 @@ -
    +
    From 765975e6671ec4c4c0b52e99f341596b0bd89fdd Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sun, 11 Jan 2015 18:17:49 -0500 Subject: [PATCH 21/28] ACP groups delete calls generic groups socket listener now, #2588 --- public/src/admin/manage/groups.js | 6 ++++-- src/socket.io/admin/groups.js | 4 ---- src/socket.io/groups.js | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/public/src/admin/manage/groups.js b/public/src/admin/manage/groups.js index 19f0bf31e9..5833f99b66 100644 --- a/public/src/admin/manage/groups.js +++ b/public/src/admin/manage/groups.js @@ -102,12 +102,14 @@ define('admin/manage/groups', [ case 'delete': bootbox.confirm('Are you sure you wish to delete this group?', function(confirm) { if (confirm) { - socket.emit('admin.groups.delete', groupName, function(err, data) { + socket.emit('groups.delete', { + groupName: groupName + }, function(err, data) { if(err) { return app.alertError(err.message); } - ajaxify.go('admin/manage/groups'); + ajaxify.refresh(); }); } }); diff --git a/src/socket.io/admin/groups.js b/src/socket.io/admin/groups.js index b78380428e..9700bee3ca 100644 --- a/src/socket.io/admin/groups.js +++ b/src/socket.io/admin/groups.js @@ -15,10 +15,6 @@ Groups.create = function(socket, data, callback) { }, callback); }; -Groups.delete = function(socket, groupName, callback) { - groups.destroy(groupName, callback); -}; - Groups.get = function(socket, groupName, callback) { groups.get(groupName, { expand: true diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 3c9dc9b069..c517cbabb2 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -101,4 +101,18 @@ SocketGroups.update = function(socket, data, callback) { }); }; +SocketGroups.delete = function(socket, data, callback) { + if(!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + if (!isOwner) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.destroy(data.groupName, callback); + }); +}; + module.exports = SocketGroups; From 635393a274d67289d0bdaf8402d1b045869649ed Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 12 Jan 2015 13:00:23 -0500 Subject: [PATCH 22/28] group creation and deletion from outside ACP, #2588 --- public/language/en_GB/groups.json | 7 ++++++- public/src/client/groups/details.js | 29 ++++++++++++++++++++++++++++- public/src/client/groups/list.js | 19 ++++++++++++++++++- src/groups.js | 2 +- src/socket.io/groups.js | 11 +++++++++++ 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/public/language/en_GB/groups.json b/public/language/en_GB/groups.json index 1dafb41ed6..f7ccec33bc 100644 --- a/public/language/en_GB/groups.json +++ b/public/language/en_GB/groups.json @@ -2,6 +2,8 @@ "groups": "Groups", "view_group": "View Group", "owner": "Group Owner", + "new_group": "Create New Group", + "no_groups_found": "There are no groups to see", "details.title": "Group Details", "details.members": "Member List", @@ -11,5 +13,8 @@ "details.private": "Private Group", "details.public": "Public Group", - "details.owner_options": "Group Administration" + "details.owner_options": "Group Administration", + + "event.updated": "Group details have been updated", + "event.deleted": "The group \"%1\" has been deleted" } \ No newline at end of file diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 0aacde97ce..d8d98a2edc 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -1,5 +1,5 @@ "use strict"; -/* globals define, socket, ajaxify, app */ +/* globals define, socket, ajaxify, app, bootbox */ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker'], function(iconSelect) { var Details = {}; @@ -38,6 +38,10 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker'], Details.update(); break; + case 'delete': + Details.deleteGroup(); + break; + case 'join': // intentional fall-throughs! case 'leave': case 'accept': @@ -118,10 +122,33 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker'], } else { ajaxify.refresh(); } + + app.alertSuccess('[[groups:event.updated'); }); }); } }; + Details.deleteGroup = function() { + bootbox.confirm('Are you sure you want to delete the group: ' + ajaxify.variables.get('group_name'), function(confirm) { + if (confirm) { + bootbox.prompt('Please enter the name of this group in order to delete it:', function(response) { + if (response === ajaxify.variables.get('group_name')) { + socket.emit('groups.delete', { + groupName: ajaxify.variables.get('group_name') + }, function(err) { + if (!err) { + app.alertSuccess('[[groups:event.deleted, ' + ajaxify.variables.get('group_name') + ']]'); + ajaxify.go('groups'); + } else { + app.alertError(err.message); + } + }); + } + }); + } + }); + }; + return Details; }); \ No newline at end of file diff --git a/public/src/client/groups/list.js b/public/src/client/groups/list.js index e7daf195f1..03243d6822 100644 --- a/public/src/client/groups/list.js +++ b/public/src/client/groups/list.js @@ -1,5 +1,5 @@ "use strict"; -/* globals define, ajaxify, socket */ +/* globals app, define, ajaxify, socket, bootbox */ define('forum/groups/list', function() { var Groups = {}; @@ -20,6 +20,23 @@ define('forum/groups/list', function() { } }); }); + + // Group creation + $('button[data-action="new"]').on('click', function() { + bootbox.prompt('Group Name:', function(name) { + if (name && name.length) { + socket.emit('groups.create', { + name: name + }, function(err) { + if (!err) { + ajaxify.go('groups/' + name); + } else { + app.alertError(err.message); + } + }); + } + }); + }); }; return Groups; diff --git a/src/groups.js b/src/groups.js index 333b823396..19560ca14c 100644 --- a/src/groups.js +++ b/src/groups.js @@ -386,7 +386,7 @@ var async = require('async'), var groupData = { name: data.name, userTitle: data.name, - description: data.description, + description: data.description || '', deleted: '0', hidden: '0', system: system ? '1' : '0', diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index c517cbabb2..3f3718d97f 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -101,6 +101,17 @@ SocketGroups.update = function(socket, data, callback) { }); }; +SocketGroups.create = function(socket, data, callback) { + if(!data) { + return callback(new Error('[[error:invalid-data]]')); + } else if (socket.uid === 0) { + return callback(new Error('[[error:no-privileges]]')); + } + + data.ownerUid = socket.uid; + groups.create(data, callback); +}; + SocketGroups.delete = function(socket, data, callback) { if(!data) { return callback(new Error('[[error:invalid-data]]')); From 675968f0f37d8fc465b86ef06a57d2db0d4ed515 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 12 Jan 2015 20:34:15 -0500 Subject: [PATCH 23/28] cover photo for groups, #2588 --- public/language/en_GB/groups.json | 5 + public/src/client/groups/details.js | 91 +++++++++- .../backgroundDraggable.js | 157 ++++++++++++++++++ src/controllers/uploads.js | 4 + src/groups.js | 82 ++++++++- src/socket.io/groups.js | 22 +++ 6 files changed, 356 insertions(+), 5 deletions(-) create mode 100644 public/vendor/jquery/draggable-background/backgroundDraggable.js diff --git a/public/language/en_GB/groups.json b/public/language/en_GB/groups.json index f7ccec33bc..eb9a45f994 100644 --- a/public/language/en_GB/groups.json +++ b/public/language/en_GB/groups.json @@ -5,6 +5,11 @@ "new_group": "Create New Group", "no_groups_found": "There are no groups to see", + "cover-instructions": "Drag and Drop a photo, drag to position, and hit Save", + "cover-change": "Change", + "cover-save": "Save", + "cover-saving": "Saving", + "details.title": "Group Details", "details.members": "Member List", "details.pending": "Pending Members", diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index d8d98a2edc..de9ec199db 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -1,14 +1,17 @@ "use strict"; -/* globals define, socket, ajaxify, app, bootbox */ +/* globals define, socket, ajaxify, app, bootbox, RELATIVE_PATH */ -define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker'], function(iconSelect) { - var Details = {}; +define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', 'vendor/jquery/draggable-background/backgroundDraggable'], function(iconSelect) { + var Details = { + cover: {} + }; Details.init = function() { var detailsPage = $('.groups'), settingsFormEl = detailsPage.find('form'); Details.prepareSettings(); + Details.initialiseCover(); $('.latest-posts .content img').addClass('img-responsive'); @@ -150,5 +153,87 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker'], }); }; + Details.initialiseCover = function() { + var coverEl = $('.group-cover'); + coverEl.find('.change').on('click', function() { + coverEl.toggleClass('active', 1); + coverEl.backgroundDraggable(); + coverEl.on('dragover', Details.cover.onDragOver); + coverEl.on('drop', Details.cover.onDrop); + }); + + coverEl.find('.save').on('click', Details.cover.save); + }; + + // Cover Photo Handling Code + + Details.cover.load = function() { + socket.emit('groups.cover.get', function(err, data) { + if (!err) { + var coverEl = $('.group-cover'); + if (data['cover:url']) { + coverEl.css('background-image', 'url(' + RELATIVE_PATH + '/theme-rocket/cover/' + data['cover:url'] + ')'); + } + + if (data['cover:position']) { + coverEl.css('background-position', data['cover:position']); + } + + delete Details.cover.newCover; + } else { + app.alertError(err.message); + } + }); + }; + + Details.cover.onDragOver = function(e) { + e.stopPropagation(); + e.preventDefault(); + e.originalEvent.dataTransfer.dropEffect = 'copy'; + }; + + Details.cover.onDrop = function(e) { + var coverEl = $('.group-cover'); + e.stopPropagation(); + e.preventDefault(); + + var files = e.originalEvent.dataTransfer.files, + reader = new FileReader(); + + if (files.length && files[0].type.match('image.*')) { + reader.onload = function(e) { + coverEl.css('background-image', 'url(' + e.target.result + ')'); + coverEl.backgroundDraggable(); + Details.cover.newCover = e.target.result; + }; + + reader.readAsDataURL(files[0]); + } + }; + + Details.cover.save = function() { + var coverEl = $('.group-cover'); + + coverEl.addClass('saving'); + + socket.emit('groups.cover.update', { + groupName: ajaxify.variables.get('group_name'), + imageData: Details.cover.newCover || undefined, + position: $('.group-cover').css('background-position') + }, function(err) { + if (!err) { + coverEl.toggleClass('active', 0); + coverEl.backgroundDraggable('disable'); + coverEl.off('dragover', Details.cover.onDragOver); + coverEl.off('drop', Details.cover.onDrop); + Details.cover.load(); + } else { + app.alertError(err.message); + } + + coverEl.removeClass('saving'); + }); + }; + return Details; }); \ No newline at end of file diff --git a/public/vendor/jquery/draggable-background/backgroundDraggable.js b/public/vendor/jquery/draggable-background/backgroundDraggable.js new file mode 100644 index 0000000000..baae4d0f95 --- /dev/null +++ b/public/vendor/jquery/draggable-background/backgroundDraggable.js @@ -0,0 +1,157 @@ +/** + * Draggable Background plugin for jQuery + * + * v1.2.4 + * + * Copyright (c) 2014 Kenneth Chung + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +;(function($) { + var $window = $(window); + + // Helper function to guarantee a value between low and hi unless bool is false + var limit = function(low, hi, value, bool) { + if (arguments.length === 3 || bool) { + if (value < low) return low; + if (value > hi) return hi; + } + return value; + }; + + // Adds clientX and clientY properties to the jQuery's event object from touch + var modifyEventForTouch = function(e) { + e.clientX = e.originalEvent.touches[0].clientX; + e.clientY = e.originalEvent.touches[0].clientY; + }; + + var getBackgroundImageDimensions = function($el) { + var bgSrc = ($el.css('background-image').match(/^url\(['"]?(.*?)['"]?\)$/i) || [])[1]; + if (!bgSrc) return; + + var imageDimensions = { width: 0, height: 0 }, + image = new Image(); + + image.onload = function() { + if ($el.css('background-size') == "cover") { + var elementWidth = $el.innerWidth(), + elementHeight = $el.innerHeight(), + elementAspectRatio = elementWidth / elementHeight; + imageAspectRatio = image.width / image.height, + scale = 1; + + if (imageAspectRatio >= elementAspectRatio) { + scale = elementHeight / image.height; + } else { + scale = elementWidth / image.width; + } + + imageDimensions.width = image.width * scale; + imageDimensions.height = image.height * scale; + } else { + imageDimensions.width = image.width; + imageDimensions.height = image.height; + } + }; + + image.src = bgSrc; + + return imageDimensions; + }; + + function Plugin(element, options) { + this.element = element; + this.options = options; + this.init(); + } + + Plugin.prototype.init = function() { + var $el = $(this.element), + bgSrc = ($el.css('background-image').match(/^url\(['"]?(.*?)['"]?\)$/i) || [])[1], + options = this.options; + + if (!bgSrc) return; + + // Get the image's width and height if bound + var imageDimensions = { width: 0, height: 0 }; + if (options.bound) { + imageDimensions = getBackgroundImageDimensions($el); + } + + $el.on('mousedown.dbg touchstart.dbg', function(e) { + if (e.target !== $el[0]) { + return; + } + e.preventDefault(); + + if (e.originalEvent.touches) { + modifyEventForTouch(e); + } else if (e.which !== 1) { + return; + } + + var x0 = e.clientX, + y0 = e.clientY, + pos = $el.css('background-position').match(/(-?\d+).*?\s(-?\d+)/) || [], + xPos = parseInt(pos[1]) || 0, + yPos = parseInt(pos[2]) || 0; + + $window.on('mousemove.dbg touchmove.dbg', function(e) { + e.preventDefault(); + + if (e.originalEvent.touches) { + modifyEventForTouch(e); + } + + var x = e.clientX, + y = e.clientY; + + xPos = options.axis === 'y' ? xPos : limit($el.innerWidth()-imageDimensions.width, 0, xPos+x-x0, options.bound); + yPos = options.axis === 'x' ? yPos : limit($el.innerHeight()-imageDimensions.height, 0, yPos+y-y0, options.bound); + x0 = x; + y0 = y; + + $el.css('background-position', xPos + 'px ' + yPos + 'px'); + }); + + $window.on('mouseup.dbg touchend.dbg mouseleave.dbg', function() { + if (options.done) { + options.done(); + } + + $window.off('mousemove.dbg touchmove.dbg'); + $window.off('mouseup.dbg touchend.dbg mouseleave.dbg'); + }); + }); + }; + + Plugin.prototype.disable = function() { + var $el = $(this.element); + $el.off('mousedown.dbg touchstart.dbg'); + $window.off('mousemove.dbg touchmove.dbg mouseup.dbg touchend.dbg mouseleave.dbg'); + } + + $.fn.backgroundDraggable = function(options) { + var options = options; + var args = Array.prototype.slice.call(arguments, 1); + + return this.each(function() { + var $this = $(this); + + if (typeof options == 'undefined' || typeof options == 'object') { + options = $.extend({}, $.fn.backgroundDraggable.defaults, options); + var plugin = new Plugin(this, options); + $this.data('dbg', plugin); + } else if (typeof options == 'string' && $this.data('dbg')) { + var plugin = $this.data('dbg'); + Plugin.prototype[options].apply(plugin, args); + } + }); + }; + + $.fn.backgroundDraggable.defaults = { + bound: true, + axis: undefined + }; +}(jQuery)); \ No newline at end of file diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 237ae5c453..b7e590b6bf 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -72,6 +72,10 @@ uploadsController.uploadThumb = function(req, res, next) { }, next); }; +uploadsController.uploadGroupCover = function(data, next) { + uploadImage(0/*req.user.uid*/, data, next); +}; + function uploadImage(uid, image, callback) { if (plugins.hasListeners('filter:uploadImage')) { return plugins.fireHook('filter:uploadImage', {image: image, uid: uid}, callback); diff --git a/src/groups.js b/src/groups.js index 19560ca14c..4e4656dd3a 100644 --- a/src/groups.js +++ b/src/groups.js @@ -3,14 +3,20 @@ var async = require('async'), winston = require('winston'), _ = require('underscore'), + crypto = require('crypto'), + path = require('path'), + nconf = require('nconf'), + fs = require('fs'), + user = require('./user'), meta = require('./meta'), db = require('./database'), plugins = require('./plugins'), posts = require('./posts'), privileges = require('./privileges'), - utils = require('../public/src/utils'); + utils = require('../public/src/utils'), + uploadsController = require('./controllers/uploads'); (function(Groups) { @@ -60,7 +66,12 @@ var async = require('async'), } return groups; - } + }/*, + fixImageUrl: function(url) { + if (url) { + return url.indexOf('http') === -1 ? nconf.get('relative_path') + url : url; + } + }*/ }; Groups.list = function(options, callback) { @@ -185,6 +196,7 @@ var async = require('async'), return callback(err); } + // results.base.image = internals.fixImageUrl(results.base.image); results.base.members = results.users.filter(Boolean); results.base.pending = results.pending.filter(Boolean); results.base.count = numUsers || results.base.members.length; @@ -203,6 +215,15 @@ var async = require('async'), }); }; + Groups.getGroupFields = function(groupName, fields, callback) { + db.getObjectFields('group:' + groupName, fields, callback); + }; + + Groups.setGroupField = function(groupName, field, value, callback) { + plugins.fireHook('action:group.set', {field: field, value: value, type: 'set'}); + db.setObjectField('group:' + groupName, field, value, callback); + }; + Groups.isPrivate = function(groupName, callback) { db.getObjectField('group:' + groupName, 'private', function(err, isPrivate) { isPrivate = isPrivate || isPrivate === null; @@ -688,6 +709,63 @@ var async = require('async'), }); }; + Groups.updateCoverPosition = function(groupName, position, callback) { + Groups.setGroupField(groupName, 'cover:position', position, callback); + }; + + Groups.updateCover = function(data, callback) { + var tempPath, md5sum, url; + + // Position only? That's fine + if (!data.imageData && data.position) { + return Groups.updateCoverPosition(data.groupName, data.position, callback); + } + + async.series([ + function(next) { + // Calculate md5sum of image + // This is required because user data can be private + md5sum = crypto.createHash('md5'); + md5sum.update(data.imageData); + md5sum = md5sum.digest('hex'); + next(); + }, + function(next) { + // Save image + tempPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), md5sum); + var buffer = new Buffer(data.imageData.slice(data.imageData.indexOf('base64') + 7), 'base64'); + + fs.writeFile(tempPath, buffer, { + encoding: 'base64' + }, next); + }, + function(next) { + uploadsController.uploadGroupCover({ + path: tempPath + }, function(err, uploadData) { + if (err) { + return next(err); + } + + url = uploadData.url; + next(); + }); + }, + function(next) { + Groups.setGroupField(data.groupName, 'cover:url', url, next); + }, + function(next) { + fs.unlink(tempPath, next); // Delete temporary file + } + ], function(err) { + if (err) { + return callback(err); + } + + Groups.updateCoverPosition(data.groupName, data.position, callback); + }); + } + Groups.ownership = {}; Groups.ownership.isOwner = function(uid, groupName, callback) { diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 3f3718d97f..c4b4535e82 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -126,4 +126,26 @@ SocketGroups.delete = function(socket, data, callback) { }); }; +SocketGroups.cover = {}; + +SocketGroups.cover.get = function(socket, data, callback) { + groups.getGroupFields(data.groupName, ['cover:url', 'cover:position'], callback); +}; + +SocketGroups.cover.update = function(socket, data, callback) { + if(!data) { + return callback(new Error('[[error:invalid-data]]')); + } else if (socket.uid === 0) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + if (!isOwner) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.updateCover(data, callback); + }); +}; + module.exports = SocketGroups; From 203a15204ee15d229c7221f07a37ec8b1c9b8d16 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 12 Jan 2015 20:40:34 -0500 Subject: [PATCH 24/28] bugfixes to cover photo, #2588 --- public/src/client/groups/details.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index de9ec199db..062ce43467 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -157,7 +157,9 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', var coverEl = $('.group-cover'); coverEl.find('.change').on('click', function() { coverEl.toggleClass('active', 1); - coverEl.backgroundDraggable(); + coverEl.backgroundDraggable({ + axis: 'y' + }); coverEl.on('dragover', Details.cover.onDragOver); coverEl.on('drop', Details.cover.onDrop); }); @@ -168,11 +170,13 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', // Cover Photo Handling Code Details.cover.load = function() { - socket.emit('groups.cover.get', function(err, data) { + socket.emit('groups.cover.get', { + groupName: ajaxify.variables.get('group_name') + }, function(err, data) { if (!err) { var coverEl = $('.group-cover'); if (data['cover:url']) { - coverEl.css('background-image', 'url(' + RELATIVE_PATH + '/theme-rocket/cover/' + data['cover:url'] + ')'); + coverEl.css('background-image', 'url(' + data['cover:url'] + ')'); } if (data['cover:position']) { From 698f598a3086d80deb7522c997eaf0e59009a08e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 12 Jan 2015 21:14:00 -0500 Subject: [PATCH 25/28] default cover photo for groups, #2588 --- public/images/cover-default.png | Bin 0 -> 33702 bytes src/groups.js | 7 ++++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 public/images/cover-default.png diff --git a/public/images/cover-default.png b/public/images/cover-default.png new file mode 100644 index 0000000000000000000000000000000000000000..8b1d392408147834387766c9212cffbf549fb0d4 GIT binary patch literal 33702 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 literal 0 HcmV?d00001 diff --git a/src/groups.js b/src/groups.js index 4e4656dd3a..a3799b4bcf 100644 --- a/src/groups.js +++ b/src/groups.js @@ -196,7 +196,12 @@ var async = require('async'), return callback(err); } - // results.base.image = internals.fixImageUrl(results.base.image); + // Default image + if (!results.base['cover:url']) { + results.base['cover:url'] = nconf.get('relative_path') + '/images/cover-default.png'; + results.base['cover:position'] = '50% 50%'; + } + results.base.members = results.users.filter(Boolean); results.base.pending = results.pending.filter(Boolean); results.base.count = numUsers || results.base.members.length; From 710df626b5b3e139336aa579eceeb77d629d5e64 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 12 Jan 2015 21:21:02 -0500 Subject: [PATCH 26/28] removed join/view buttons from group list, made cover clickable, #2588 --- public/src/client/groups/list.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/public/src/client/groups/list.js b/public/src/client/groups/list.js index 03243d6822..3fc76a4962 100644 --- a/public/src/client/groups/list.js +++ b/public/src/client/groups/list.js @@ -7,18 +7,10 @@ define('forum/groups/list', function() { Groups.init = function() { var groupsEl = $('.groups.row'); - // Group joining and leaving - groupsEl.on('click', '[data-action]', function() { - var action = $(this).attr('data-action'), - groupName = $(this).parents('[data-group]').attr('data-group'); + groupsEl.on('click', '.list-cover', function() { + var groupName = $(this).parents('[data-group]').attr('data-group'); - socket.emit('groups.' + action, { - groupName: groupName - }, function(err) { - if (!err) { - ajaxify.refresh(); - } - }); + ajaxify.go('groups/' + groupName); }); // Group creation From de58f4ff6bf4713796cccf477463b07d473edc95 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 12 Jan 2015 22:47:58 -0500 Subject: [PATCH 27/28] hiding group admin and cover photo controls from non-owners, #2588 --- public/src/client/groups/details.js | 41 +++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 062ce43467..d4effed12d 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -10,8 +10,10 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', var detailsPage = $('.groups'), settingsFormEl = detailsPage.find('form'); - Details.prepareSettings(); - Details.initialiseCover(); + if (ajaxify.variables.get('is_owner') === 'true') { + Details.prepareSettings(); + Details.initialiseCover(); + } $('.latest-posts .content img').addClass('img-responsive'); @@ -73,26 +75,24 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', previewIcon = previewEl.find('i'), previewValueEl = settingsFormEl.find('[name="icon"]'); - if (settingsFormEl.length) { - // Add color picker to settings form - colorBtn.ColorPicker({ - color: colorValueEl.val() || '#000', - onChange: function(hsb, hex) { - colorValueEl.val('#' + hex); - previewEl.css('background-color', '#' + hex); - }, - onShow: function(colpkr) { - $(colpkr).css('z-index', 1051); - } - }); + // Add color picker to settings form + colorBtn.ColorPicker({ + color: colorValueEl.val() || '#000', + onChange: function(hsb, hex) { + colorValueEl.val('#' + hex); + previewEl.css('background-color', '#' + hex); + }, + onShow: function(colpkr) { + $(colpkr).css('z-index', 1051); + } + }); - // Add icon selection interface - iconBtn.on('click', function() { - iconSelect.init(previewIcon, function() { - previewValueEl.val(previewIcon.val()); - }); + // Add icon selection interface + iconBtn.on('click', function() { + iconSelect.init(previewIcon, function() { + previewValueEl.val(previewIcon.val()); }); - } + }); }; Details.update = function() { @@ -165,6 +165,7 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', }); coverEl.find('.save').on('click', Details.cover.save); + coverEl.addClass('initialised') }; // Cover Photo Handling Code From b3b3300bd61d9d5b5812652ce47f50a50dc7e814 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 13 Jan 2015 00:25:44 -0500 Subject: [PATCH 28/28] fixing bug with checkboxes, #2588 --- public/src/client/groups/details.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index d4effed12d..62371368e8 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -96,19 +96,18 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', }; Details.update = function() { - var settingsFormEl = $('.groups form'); + var settingsFormEl = $('.groups form'), + checkboxes = settingsFormEl.find('input[type="checkbox"][name]'); if (settingsFormEl.length) { require(['vendor/jquery/serializeObject/jquery.ba-serializeobject.min'], function() { - var settings = settingsFormEl.serializeObject(), - keys = Object.keys(settings), - inputEl; + var settings = settingsFormEl.serializeObject(); // Fix checkbox values - keys.forEach(function(key) { - inputEl = settingsFormEl.find('input[type="checkbox"][name="' + key + '"]'); + checkboxes.each(function(idx, inputEl) { + inputEl = $(inputEl); if (inputEl.length) { - settings[key] = settings[key] === 'on' ? true : false; + settings[inputEl.attr('name')] = inputEl.prop('checked'); } }); @@ -126,7 +125,7 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', ajaxify.refresh(); } - app.alertSuccess('[[groups:event.updated'); + app.alertSuccess('[[groups:event.updated]]'); }); }); } @@ -153,6 +152,8 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', }); }; + // Cover Photo Handling Code + Details.initialiseCover = function() { var coverEl = $('.group-cover'); coverEl.find('.change').on('click', function() { @@ -165,11 +166,9 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', }); coverEl.find('.save').on('click', Details.cover.save); - coverEl.addClass('initialised') + coverEl.addClass('initialised'); }; - // Cover Photo Handling Code - Details.cover.load = function() { socket.emit('groups.cover.get', { groupName: ajaxify.variables.get('group_name')