Team invitation

pull/1/head
yflory 5 years ago
parent 3a76e84286
commit e4e2c3a19d

@ -30,7 +30,7 @@
// These show only once
.alertify-logs {
z-index: 10000; // alertify logs
z-index: 100001; // alertify logs should be in front of alertify modals
@media print {
visibility: hidden;
}
@ -339,7 +339,7 @@
.alertify-logs {
position: fixed;
z-index: 99999; // alertify logs
z-index: 100001; // alertify logs (just in front of alertify modals
&.bottom, &:not(.top) {
bottom: 16px;
@ -459,8 +459,15 @@
}
}
}
.cp-ownership {
& > label {
font-weight: bold;
}
}
}
.cp-share-column {
.cp-share-grid, .cp-share-list {
.avatar_main(50px);
.avatar_main(40px);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
@ -513,6 +520,9 @@
color: @colortheme_alertify-primary-text;
order: -1 !important;
}
.cp-share-friend-avatar {
min-height: 40px;
}
.cp-share-friend-name {
overflow: hidden;
white-space: nowrap;
@ -525,11 +535,6 @@
visibility: hidden;
}
}
.cp-ownership {
& > label {
font-weight: bold;
}
}
}
}

@ -10,7 +10,7 @@ define([
var Msg = {};
var createData = Msg.createData = function (proxy, hash) {
return {
var data = {
channel: hash || Hash.createChannelId(),
displayName: proxy['cryptpad.username'],
profile: proxy.profile && proxy.profile.view,
@ -19,6 +19,8 @@ define([
notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']),
avatar: proxy.profile && proxy.profile.avatar
};
if (hash === false) { delete data.channel; }
return data;
};
var getFriend = Msg.getFriend = function (proxy, pubkey) {

@ -146,18 +146,6 @@ define([
}, function () {
});
var $div = $(removeCol.div);
var others1 = removeCol.others;
$div.append(h('div.cp-share-grid', others1));
$div.find('.cp-share-friend').click(function () {
var sel = $(this).hasClass('cp-selected');
if (!sel) {
$(this).addClass('cp-selected');
} else {
var order = $(this).attr('data-order');
order = order ? 'order:'+order : '';
$(this).removeClass('cp-selected').attr('style', order);
}
});
// When clicking on the remove button, we check the selected users.
// If you try to remove yourself, we'll display an additional warning message
var btnMsg = pending ? Messages.owner_removePendingButton : Messages.owner_removeButton;
@ -244,18 +232,6 @@ define([
//console.log(arguments);
});
$div2 = $(addCol.div);
var others2 = addCol.others;
$div2.append(h('div.cp-share-grid', others2));
$div2.find('.cp-share-friend').click(function () {
var sel = $(this).hasClass('cp-selected');
if (!sel) {
$(this).addClass('cp-selected');
} else {
var order = $(this).attr('data-order');
order = order ? 'order:'+order : '';
$(this).removeClass('cp-selected').attr('style', order);
}
});
// When clicking on the add button, we get the selected users.
var addButton = h('button.no-margin', Messages.owner_addButton);
$(addButton).click(function () {
@ -724,6 +700,19 @@ define([
onSelect();
});
$(div).append(h('div.cp-share-grid', others));
$div.on('click', '.cp-share-friend', function () {
var sel = $(this).hasClass('cp-selected');
if (!sel) {
$(this).addClass('cp-selected');
} else {
var order = $(this).attr('data-order');
order = order ? 'order:'+order : '';
$(this).removeClass('cp-selected').attr('style', order);
}
onSelect();
});
return {
others: others,
div: div
@ -749,7 +738,7 @@ define([
// Replace "copy link" by "share with friends" if at least one friend is selected
// Also create the "share with friends" button if it doesn't exist
var refreshButtons = function () {
var $nav = $div.parents('.alertify').find('nav');
var $nav = $div.closest('.alertify').find('nav');
var friendMode = $div.find('.cp-share-friend.cp-selected').length;
if (friendMode) {
@ -759,6 +748,7 @@ define([
}
};
config.noInclude = true;
var friendsList = UIElements.getFriendsList(Messages.share_linkFriends, config, refreshButtons);
var friendDiv = friendsList.div;
$div.append(friendDiv);
@ -784,7 +774,6 @@ define([
friends: teams
}, refreshButtons);
$div.append(teamsList.div);
$(teamsList.div).append(h('div.cp-share-grid', teamsList.others));
var shareButtons = [{
className: 'primary cp-share-with-friends',
@ -870,19 +859,9 @@ define([
$(el).attr('data-order', i).css('order', i);
});
// Display them
$(friendDiv).find('.cp-share-grid').detach();
$(friendDiv).append(h('div.cp-share-grid', others));
$div.append(UI.dialog.getButtons(shareButtons, config.onClose));
$div.find('.cp-share-friend').click(function () {
var sel = $(this).hasClass('cp-selected');
if (!sel) {
$(this).addClass('cp-selected');
} else {
var order = $(this).attr('data-order');
order = order ? 'order:'+order : '';
$(this).removeClass('cp-selected').attr('style', order);
}
refreshButtons();
});
refreshButtons();
});
return div;
@ -903,7 +882,6 @@ define([
var friendsUIClass = hasFriends ? '.cp-share-columns' : '';
var mainShareColumn = h('div.cp-share-column.contains-nav', [
hasFriends ? h('p', Messages.share_description) : undefined,
h('label', Messages.share_linkAccess),
h('br'),
UI.createRadio('cp-share-editable', 'cp-share-editable-true',
@ -1195,6 +1173,78 @@ define([
return UI.dialog.customModal(link, {buttons: linkButtons});
};
UIElements.createInviteTeamModal = function (config) {
var common = config.common;
var hasFriends = Object.keys(config.friends || {}).length !== 0;
var friendsList = hasFriends ? createShareWithFriends(config) : undefined;
if (!hasFriends) {
return void UI.alert('No friend to invite'); // XXX
}
var privateData = common.getMetadataMgr().getPrivateData();
var team = privateData.teams[config.teamId];
if (!team) { return void UI.warn(Messages.error); }
var module = config.module || common.makeUniversal('team', { onEvent: function () {} });
var $div;
var refreshButton = function () {
if (!$div) { return; }
var $modal = $div.closest('.alertify');
var $nav = $modal.find('nav');
var $btn = $nav.find('button.primary');
var selected = $div.find('.cp-share-friend.cp-selected').length;
if (selected) {
$btn.prop('disabled', '');
} else {
$btn.prop('disabled', 'disabled');
}
};
var list = UIElements.getFriendsList('Pick the friends you want to invite to the team', { // XXX
common: common,
friends: config.friends,
}, refreshButton);
$div = $(list.div);
refreshButton();
var buttons = [{
className: 'cancel',
name: Messages.cancel,
onClick: function () {},
keys: [27]
}, {
className: 'primary',
name: 'INVITE', // XXX
onClick: function () {
var $sel = $div.find('.cp-share-friend.cp-selected');
var sel = $sel.toArray();
if (!sel.length) { return; }
var friends = sel.forEach(function (el) {
var curve = $(el).attr('data-curve');
module.execCommand('INVITE_TO_TEAM', {
teamId: config.teamId,
user: config.friends[curve]
}, function (obj) {
if (obj && obj.error) {
console.error(obj.error);
return UI.warn(Messages.error);
}
});
});
},
keys: [13]
}];
var content = h('div', [
h('h4', 'Invite friends to your team: '+ team.name),
list.div
]);
var modal = UI.dialog.customModal(content, {buttons: buttons});
UI.openCustomModal(modal);
};
UIElements.createButton = function (common, type, rightside, data, callback) {
var AppConfig = common.getAppConfig();
var button;
@ -3503,27 +3553,7 @@ define([
var content = h('div.cp-share-modal', [
setHTML(h('p'), text)
]);
var buttons = [{
name: Messages.friendRequest_later,
onClick: function () {},
keys: [27]
}, {
className: 'primary',
name: Messages.friendRequest_accept,
onClick: function () {
todo(true);
},
keys: [13]
}, {
className: 'primary',
name: Messages.friendRequest_decline,
onClick: function () {
todo(false);
},
keys: [[13, 'ctrl']]
}];
var modal = UI.dialog.customModal(content, {buttons: buttons});
UI.openCustomModal(modal);
UI.proposal(content, todo);
};
UIElements.displayAddOwnerModal = function (common, data) {
@ -3648,27 +3678,85 @@ define([
});
};
var buttons = [{
name: Messages.friendRequest_later,
onClick: function () {},
keys: [27]
}, {
className: 'primary',
name: Messages.friendRequest_accept,
onClick: function () {
todo(true);
},
keys: [13]
UI.proposal(div, todo);
};
UIElements.getVerifiedFriend = function (common, curve, name) {
var priv = common.getMetadataMgr().getPrivateData();
var verified = h('p');
var $verified = $(verified);
if (priv.friends && priv.friends[curve]) {
$verified.addClass('cp-notifications-requestedit-verified');
var f = priv.friends[curve];
$verified.append(h('span.fa.fa-certificate'));
var $avatar = $(h('span.cp-avatar')).appendTo($verified);
$verified.append(h('p', Messages._getKey('requestEdit_fromFriend', [f.displayName])));
common.displayAvatar($avatar, f.avatar, f.displayName);
} else {
$verified.append(Messages._getKey('requestEdit_fromStranger', [name]));
}
return verified;
};
UIElements.displayInviteTeamModal = function (common, data) {
var priv = common.getMetadataMgr().getPrivateData();
var user = common.getMetadataMgr().getUserData();
var sframeChan = common.getSframeChannel();
var msg = data.content.msg;
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name']) || '');
var verified = UIElements.getVerifiedFriend(common, msg.author, name);
//var text = Messages._getKey('', [name, title]); // XXX
var text = name + " has invited you to join the team <b>" + teamName +"</b>";
var div = h('div', [
UI.setHTML(h('p'), text),
verified
]);
var module = common.makeUniversal('team');
var answer = function (yes) {
common.mailbox.sendTo("INVITE_TO_TEAM_ANSWER", {
answer: yes,
teamChannel: msg.content.team.channel,
user: {
displayName: user.name,
avatar: user.avatar,
profile: user.profile,
notifications: user.notifications,
curvePublic: user.curvePublic,
edPublic: priv.edPublic
}
}, {
className: 'primary',
name: Messages.friendRequest_decline,
onClick: function () {
todo(false);
},
keys: [[13, 'ctrl']]
}];
var modal = UI.dialog.customModal(div, {buttons: buttons});
UI.openCustomModal(modal);
channel: msg.content.user.notifications,
curvePublic: msg.content.user.curvePublic
});
common.mailbox.dismiss(data, function (err) {
console.log(err);
});
};
var todo = function (yes) {
if (yes) {
// ACCEPT
module.execCommand('JOIN_TEAM', {
team: msg.content.team
}, function (obj) {
if (obj && obj.error) { return void UI.warn(Messages.error); }
answer(true);
});
return;
}
// DECLINE
answer(false);
};
UI.proposal(div, todo);
};
return UIElements;

@ -145,22 +145,10 @@ define([
var link = h('a', {
href: '#'
}, Messages.requestEdit_viewPad);
var verified = h('p');
var $verified = $(verified);
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
if (priv.friends && priv.friends[msg.author]) {
$verified.addClass('cp-notifications-requestedit-verified');
var f = priv.friends[msg.author];
$verified.append(h('span.fa.fa-certificate'));
var $avatar = $(h('span.cp-avatar')).appendTo($verified);
$verified.append(h('p', Messages._getKey('requestEdit_fromFriend', [f.displayName])));
common.displayAvatar($avatar, f.avatar, f.displayName);
} else {
$verified.append(Messages._getKey('requestEdit_fromStranger', [name]));
}
var verified = UIElements.getVerifiedFriend(common, msg.author, name);
var div = h('div', [
UI.setHTML(h('p'), Messages._getKey('requestEdit_confirm', [title, name])),
@ -268,6 +256,42 @@ define([
}
};
handlers['INVITE_TO_TEAM'] = function (common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name']) || '');
content.getFormatText = function () {
var text = name + " has invited you to join the team <b>" + teamName +"</b>";
return text;
};
if (!content.archived) {
content.handler = function () {
UIElements.displayInviteTeamModal(common, data);
};
}
};
handlers['INVITE_TO_TEAM_ANSWER'] = function (common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name']) || '');
var key = 'owner_request_' + (msg.content.answer ? 'accepted' : 'declined');
content.getFormatText = function () {
//return Messages._getKey(key, [name, title]); // XXX
return name +' has ' + (msg.content.answer ? 'accepted' : 'declined') + ' your offer to join the team <b>' + teamName + '</b>';
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
// NOTE: don't forget to fixHTML everything returned by "getFormatText"
return {

@ -1,7 +1,8 @@
define([
'/common/common-messaging.js',
'/common/common-hash.js',
], function (Messaging, Hash) {
'/common/common-util.js',
], function (Messaging, Hash, Util) {
var getRandomTimeout = function (ctx) {
var lag = ctx.store.realtime.getLag().lag || 0;
@ -309,6 +310,72 @@ define([
cb(false);
};
var invitedTo = {};
handlers['INVITE_TO_TEAM'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (!content.team) {
console.log('Remove invalid notification');
return void cb(true);
}
if (invitedTo[content.team.channel]) { return void cb(true); }
var myTeams = Util.find(ctx, ['store', 'proxy', 'teams'])
var alreadyMember = Object.keys(myTeams).some(function (k) {
var team = myTeams[k];
return team.channel === content.team.channel;
});
if (alreadyMember) { return void cb(true); }
invitedTo[content.team.channel] = true;
cb(false);
};
removeHandlers['INVITE_TO_TEAM'] = function (ctx, box, data) {
var channel = Util.find(data, ['content', 'team', 'channel']);
delete invitedTo[channel];
};
handlers['INVITE_TO_TEAM_ANSWER'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (!content.teamChannel) {
console.log('Remove invalid notification');
return void cb(true);
}
var myTeams = Util.find(ctx, ['store', 'proxy', 'teams'])
var teamId;
var team;
Object.keys(myTeams).some(function (k) {
var _team = myTeams[k];
if (_team.channel === content.teamChannel) {
teamId = k;
team = _team;
return true;
}
});
if (!teamId) { return void cb(true); }
content.team = team;
if (!content.answer) {
// If they declined the invitation, remove them from the roster (as a pending member)
try {
var module = ctx.store.modules['team'];
module.removeFromTeam(teamId, msg.author);
} catch (e) { console.error(e); }
}
cb(false);
};
return {
add: function (ctx, box, data, cb) {
/**

@ -310,8 +310,8 @@ define([
// If you're allowed to edit the roster, try to update your data
if (!rosterData.edit) { return; }
var data = {};
var myData = Messaging.createData(ctx.store.proxy);
delete myData.channel;
var myData = Messaging.createData(ctx.store.proxy, false);
myData.pending = false;
data[ctx.store.proxy.curvePublic] = myData;
roster.describe(data, function (err) {
if (!err) { return; }
@ -448,6 +448,19 @@ define([
});
};
var joinTeam = function (ctx, data, cId, cb) {
var team = data.team;
if (!team.hash || !team.channel || !team.password
|| !team.keys || !team.metadata) { return void cb({error: 'EINVAL'}); }
var id = Util.createRandomInteger();
ctx.store.proxy.teams[id] = team;
ctx.onReadyHandlers[id] = [];
openChannel(ctx, team, id, function (obj) {
if (!(obj && obj.error)) { console.debug('Team joined:' + id); }
cb(obj);
});
};
var getTeamRoster = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
@ -499,6 +512,43 @@ define([
});
};
// TODO send guest keys only in the future
var getInviteData = function (ctx, teamId) {
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return; }
var data = Util.clone(teamData);
delete data.owner;
return data;
};
var inviteToTeam = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var user = data.user;
if (!user || !user.curvePublic || !user.notifications) { return void cb({error: 'MISSING_DATA'}); }
delete user.channel;
delete user.lastKnownHash;
user.pending = true;
var obj = {};
obj[user.curvePublic] = user;
team.roster.add(obj, function (err) {
if (err && err !== 'NO_CHANGE') { return void cb({error: err}); }
ctx.store.mailbox.sendTo('INVITE_TO_TEAM', {
user: Messaging.createData(ctx.store.proxy, false),
team: getInviteData(ctx, teamId)
}, {
channel: user.notifications,
curvePublic: user.curvePublic
}, function (obj) {
cb(obj);
});
});
};
// XXX Listen for changes to the roster pad to know if you've been removed
// XXX onReady, if you've been removed, leave the team
var removeUser = function (ctx, data, cId, cb) {
@ -584,7 +634,8 @@ define([
pinPads: cfg.pinPads,
emit: emit,
onReadyHandlers: {},
teams: {}
teams: {},
updateMetadata: cfg.updateMetadata
};
var teams = store.proxy.teams = store.proxy.teams || {};
@ -608,7 +659,8 @@ define([
Object.keys(teams).forEach(function (id) {
t[id] = {
name: teams[id].metadata.name,
edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic'])
edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']),
avatar: Util.find(teams[id], ['metadata', 'avatar'])
};
});
return t;
@ -616,6 +668,22 @@ define([
team.getTeams = function () {
return Object.keys(ctx.teams);
};
team.removeFromTeam = function (teamId, curve) {
if (!teams[teamId]) { return; }
if (ctx.onReadyHandlers[teamId]) {
ctx.onReadyHandlers[teamId].push({cb : function () {
ctx.teams[teamId].roster.remove([curve], function (err) {
if (err && err !== 'NO_CHANGE') { console.error(err); }
});
}});
return;
}
if (!ctx.teams[teamId]) { return void console.error("TEAM MODULE ERROR"); }
ctx.teams[teamId].roster.remove([curve], function (err) {
if (err && err !== 'NO_CHANGE') { console.error(err); }
});
};
team.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
@ -644,6 +712,12 @@ define([
if (cmd === 'DESCRIBE_USER') {
return void describeUser(ctx, data, clientId, cb);
}
if (cmd === 'INVITE_TO_TEAM') {
return void inviteToTeam(ctx, data, clientId, cb);
}
if (cmd === 'JOIN_TEAM') {
return void joinTeam(ctx, data, clientId, cb);
}
if (cmd === 'REMOVE_USER') {
return void removeUser(ctx, data, clientId, cb);
}

@ -167,9 +167,11 @@ define([
// Universal direct channel
var modules = {};
funcs.makeUniversal = function (type, cfg) {
if (cfg && cfg.onEvent) {
modules[type] = {
onEvent: cfg.onEvent || function () {}
};
}
var sframeChan = funcs.getSframeChannel();
return {
execCommand: function (cmd, data, cb) {

@ -397,7 +397,7 @@ define([
var theirRole = ROLES.indexOf(data.role) || 0;
// If they're a member and I have a higher role than them, I can promote them to admin
if (!isMe && myRole > theirRole && theirRole === 0) {
var promote = h('fa.fa-angle-double-up', {
var promote = h('span.fa.fa-angle-double-up', {
title: 'Promote' // XXX
});
$(promote).click(function () {
@ -410,7 +410,7 @@ define([
// If I'm not a member and I have an equal or higher role than them, I can demote them
// (if they're not already a MEMBER)
if (!isMe && myRole >= theirRole && theirRole > 0) {
var demote = h('fa.fa-angle-double-down', {
var demote = h('span.fa.fa-angle-double-down', {
title: 'Demote' // XXX
});
$(demote).click(function () {
@ -422,7 +422,7 @@ define([
}
// If I'm not a member and I have an equal or higher role than them, I can remove them
if (!isMe && myRole > 0 && myRole >= theirRole) {
var remove = h('fa.fa-times', {
var remove = h('span.fa.fa-times', {
title: 'Remove' // XXX
});
$(remove).click(function () {
@ -460,8 +460,8 @@ define([
APP.refreshRoster = function (common, roster) {
if (!roster || typeof(roster) !== "object" || Object.keys(roster) === 0) { return; }
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var me = roster[privateData.curvePublic];
var userData = metadataMgr.getUserData();
var me = roster[userData.curvePublic] || {};
var owner = Object.keys(roster).filter(function (k) {
return roster[k].role === "OWNER";
}).map(function (k) {
@ -479,7 +479,35 @@ define([
});
// XXX LEAVE the team button
// XXX INVITE to the team button
var header = h('div.cp-app-team-roster-header');
var $header = $(header);
// If you're an admin or an owner, you can invite your friends to the team
// TODO and acquaintances later?
if (me && (me.role === 'ADMIN' || me.role === 'OWNER')) {
var invite = h('button.btn.btn-primary', 'INVITE A FRIEND');
var inviteFriends = common.getFriends();
Object.keys(inviteFriends).forEach(function (curve) {
// Keep only friends that are not already in the team and that you can contact
// via their mailbox
if (roster[curve] && !roster[curve].pending) {
delete inviteFriends[curve];
}
});
var inviteCfg = {
teamId: APP.team,
common: common,
friends: inviteFriends,
module: APP.module
};
$(invite).click(function () {
UIElements.createInviteTeamModal(inviteCfg);
});
$header.append(invite);
}
return [
header,
h('h3', 'OWNER'), // XXX
h('div', owner),
h('h3', 'ADMINS'), // XXX

Loading…
Cancel
Save