diff --git a/customize.dist/src/less2/include/creation.less b/customize.dist/src/less2/include/creation.less index 281e15e44..ccf4ce5b1 100644 --- a/customize.dist/src/less2/include/creation.less +++ b/customize.dist/src/less2/include/creation.less @@ -2,6 +2,7 @@ @import (reference) "./colortheme-all.less"; @import (reference) "./tools.less"; @import (reference) './icon-colors.less'; +@import (reference) "./avatar.less"; .creation_vars( @color: @colortheme_default-color, @@ -62,7 +63,7 @@ outline: none; width: 700px; max-width: 90vw; - height: 500px; + height: 550px; max-height: calc(~"100vh - 20px"); margin: 50px; flex-shrink: 0; @@ -175,15 +176,47 @@ color: @colortheme_form-color; } - .cp-creation-team { - .cp-dropdown-container { + .cp-creation-teams { + display: none !important; + .cp-creation-teams-grid { + display: flex; + flex-wrap: wrap; + padding: 0 2px; flex: 1; - min-width: 0; - margin-left: 10px; - margin-right: 10px; - button, .cp-dropdown-content { + } + .cp-creation-team { + .avatar_main(25px); + width: 140px; + height: 35px; + display: flex; + justify-content: center; + align-items: center; + padding: 5px; + cursor: default; + font: @colortheme_app-font; + color: @colortheme_modal-fg; + margin: 0 1px; + + .tools_unselectable(); + + &.cp-selected { + background-color: @colortheme_alertify-primary; + color: @colortheme_alertify-primary-text; + } + .cp-creation-team-avatar { + .fa { + font-size: 25px; + } + } + .cp-creation-team-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; width: 100%; + text-align: center; + line-height: 18px; } + border: 1px solid @colortheme_alertify-primary; } } diff --git a/www/common/common-constants.js b/www/common/common-constants.js index 752298841..f34241b0a 100644 --- a/www/common/common-constants.js +++ b/www/common/common-constants.js @@ -16,6 +16,7 @@ define(function () { tokenKey: 'loginToken', displayPadCreationScreen: 'displayPadCreationScreen', deprecatedKey: 'deprecated', + MAX_TEAMS_SLOTS: 3, // Sub plan: 'CryptPad_plan', // Apps diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js index 8eec5c4a3..91322f526 100644 --- a/www/common/common-messaging.js +++ b/www/common/common-messaging.js @@ -81,10 +81,13 @@ define([ }; Msg.updateMyData = function (store, curve) { - var myData = createData(store.proxy); + var myData = createData(store.proxy, false); if (store.proxy.friends) { store.proxy.friends.me = myData; } + if (store.modules['team']) { + store.modules['team'].updateMyData(myData); + } var todo = function (friend) { if (!friend || !friend.notifications) { return; } myData.channel = friend.channel; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 513cf445f..a95e1c672 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2881,57 +2881,49 @@ define([ // Team pad var team; var teamExists = privateData.teams && Object.keys(privateData.teams).length; - var $teamBlock; + var teamValue; // storeInTeam can be // * a team ID ==> store in the team drive, and the team will be the owner // * -1 ==> store in the user drive, and the user will be the owner // * undefined ==> ask if (teamExists && privateData.enableTeams) { - var teamOptions = Object.keys(privateData.teams).map(function (teamId) { - var t = privateData.teams[teamId]; - return { - tag: 'a', - attributes: { - 'data-value': teamId, - 'href': '#' - }, - content: Util.fixHTML(t.name) - }; - }); - teamOptions.unshift({ - tag: 'a', - attributes: { - 'data-value': '-1', - 'href': '#' - }, - content: Messages.settings_cat_drive - }); - teamOptions.unshift({ - tag: 'a', - attributes: { - 'data-value': '', - 'href': '#' - }, - content: ' ' - }); - var teamDropdownConfig = { - text: " ", // Button initial text - options: teamOptions, // Entries displayed in the menu - isSelect: true, - common: common - }; - $teamBlock = UIElements.createDropdown(teamDropdownConfig); - $teamBlock.find('a').click(function () { - var id = $(this).attr('data-value'); - $teamBlock.setValue(id); - }); - team = h('div.cp-creation-team', [ + var teams = Object.keys(privateData.teams).map(function (id) { + var data = privateData.teams[id]; + var avatar = h('span.cp-creation-team-avatar.cp-avatar'); + UIElements.displayAvatar(common, $(avatar), data.avatar, data.name); + return h('div.cp-creation-team', { + 'data-id': id, + title: data.name, + },[ + avatar, + h('span.cp-creation-team-name', data.name) + ]); + }); + teams.unshift(h('div.cp-creation-team', { + 'data-id': '-1', + title: Messages.settings_cat_drive + }, [ + h('span.cp-creation-team-avatar.fa.fa-hdd-o'), + h('span.cp-creation-team-name', Messages.settings_cat_drive) + ])); + team = h('div.cp-creation-teams', [ Messages.team_pcsSelectLabel, - $teamBlock[0], + h('div.cp-creation-teams-grid', teams), createHelper('#', Messages.team_pcsSelectHelp) ]); + var $team = $(team); + $team.find('.cp-creation-team').click(function () { + if ($(this).hasClass('cp-selected')) { + teamValue = undefined; + return void $(this).removeClass('cp-selected'); + } + $team.find('.cp-creation-team').removeClass('cp-selected'); + $(this).addClass('cp-selected'); + teamValue = $(this).attr('data-id'); + }); if (privateData.storeInTeam) { - $teamBlock.setValue(privateData.storeInTeam); + $team.find('[data-id="'+privateData.storeInTeam+'"]').addClass('cp-selected'); + teamValue = privateData.storeInTeam; } } @@ -3208,9 +3200,9 @@ define([ var templateId = $template.data('id') || undefined; // Team var team; - if ($teamBlock && $teamBlock.getValue()) { - team = privateData.teams[$teamBlock.getValue()] || {}; - team.id = Number($teamBlock.getValue()); + if (teamValue) { + team = privateData.teams[teamValue] || {}; + team.id = Number(teamValue); } return { @@ -3746,8 +3738,15 @@ define([ console.log(err); }); }; + + var MAX_TEAMS_SLOTS = Constants.MAX_TEAMS_SLOTS; var todo = function (yes) { + var priv = common.getMetadataMgr().getPrivateData(); + var numberOfTeams = Object.keys(priv.teams || {}).length; if (yes) { + if (numberOfTeams >= MAX_TEAMS_SLOTS) { + return void UI.alert(Messages._getKey('team_maxTeams', [MAX_TEAMS_SLOTS])); + } // ACCEPT module.execCommand('JOIN_TEAM', { team: msg.content.team diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index e3b3639a3..d25cb283b 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -321,7 +321,16 @@ define([ return void cb(true); } - if (invitedTo[content.team.channel]) { return void cb(true); } + var invited = invitedTo[content.team.channel]; + if (invited) { + console.log('removing old invitation'); + cb(false, invited); + invitedTo[content.team.channel] = { + type: box.type, + hash: data.hash + }; + return; + } var myTeams = Util.find(ctx, ['store', 'proxy', 'teams']) || {}; var alreadyMember = Object.keys(myTeams).some(function (k) { @@ -330,7 +339,10 @@ define([ }); if (alreadyMember) { return void cb(true); } - invitedTo[content.team.channel] = true; + invitedTo[content.team.channel] = { + type: box.type, + hash: data.hash + }; cb(false); }; @@ -349,6 +361,10 @@ define([ return void cb(true); } + if (invitedTo[content.teamChannel] && content.pending) { + return void cb(true, invitedTo[content.teamChannel]); + } + cb(false); }; diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index 34b3b13f0..1def668e1 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -258,6 +258,11 @@ proxy.mailboxes = { hash: hash }; Handlers.add(ctx, box, message, function (dismissed, toDismiss) { + if (toDismiss) { // List of other messages to remove + dismiss(ctx, toDismiss, '', function () { + console.log('Notification handled automatically'); + }); + } if (dismissed) { // This message should be removed dismiss(ctx, { type: type, @@ -267,11 +272,6 @@ proxy.mailboxes = { }); return; } - if (toDismiss) { // List of other messages to remove - dismiss(ctx, toDismiss, '', function () { - console.log('Notification handled automatically'); - }); - } box.content[hash] = msg; showMessage(ctx, type, message, null, function (obj) { if (!box.ready) { return; } diff --git a/www/common/outer/messenger.js b/www/common/outer/messenger.js index a7ef27837..49e97915a 100644 --- a/www/common/outer/messenger.js +++ b/www/common/outer/messenger.js @@ -477,6 +477,7 @@ define([ sending: false, messages: [], clients: data.clients || [], + onUserlistUpdate: data.onUserlistUpdate || function () {}, mapId: {}, }; @@ -584,10 +585,32 @@ define([ }); }; + var getOnlineList = function (ctx, chanId) { + var channel = ctx.channels[chanId]; + if (!channel) { return; } + var online = []; // Store online members to avoid duplicates + + // Add ourselves + var myData = createData(ctx.store.proxy, false); + online.push(myData.curvePublic); + + channel.wc.members.forEach(function (nId) { + if (nId === ctx.store.network.historyKeeper) { return; } + var data = channel.mapId[nId] || {}; + if (!data.curvePublic) { return; } + if (online.indexOf(data.curvePublic) !== -1) { return; } + online.push(data.curvePublic); + }); + return online; + }; + // Display green status if one member is not me var getStatus = function (ctx, chanId, cb) { var channel = ctx.channels[chanId]; if (!channel) { return void cb('NO_SUCH_CHANNEL'); } + if (channel.onUserlistUpdate) { + channel.onUserlistUpdate(); + } var proxy = ctx.store.proxy; var online = channel.wc.members.some(function (nId) { if (nId === ctx.store.network.historyKeeper) { return; } @@ -781,7 +804,7 @@ define([ openChannel(ctx, chanData); }; - var openTeamChat = function (ctx, clientId, data, _cb) { + var openTeamChat = function (ctx, clientId, data, onUpdate, _cb) { var chatData = data; var chanId = chatData.channel; var secret = chatData.secret; @@ -820,6 +843,7 @@ define([ return encryptor.decrypt(msg, vKey); }, clients: [clientId], + onUserlistUpdate: onUpdate, onReady: cb }; openChannel(ctx, chanData); @@ -927,6 +951,10 @@ define([ onFriendRemoved(ctx, curvePublic, chanId); }; + messenger.getOnlineList = function (chanId) { + return getOnlineList(ctx, chanId); + }; + messenger.storeValidateKey = function (chan, key) { ctx.validateKeys[chan] = key; }; @@ -945,8 +973,8 @@ define([ }); }; - messenger.openTeamChat = function (data, cId, cb) { - openTeamChat(ctx, cId, data, cb); + messenger.openTeamChat = function (data, onUpdate, cId, cb) { + openTeamChat(ctx, cId, data, onUpdate, cb); }; messenger.removeClient = function (clientId) { @@ -964,9 +992,6 @@ define([ if (cmd === 'GET_USERLIST') { return void getUserList(ctx, data, cb); } - if (cmd === 'OPEN_TEAM_CHAT') { - return void openTeamChat(ctx, clientId, data, cb); - } if (cmd === 'OPEN_PAD_CHAT') { return void openPadChat(ctx, clientId, data, cb); } diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 32cb1e617..810e0b3c0 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -544,7 +544,18 @@ define([ if (!team) { return void cb ({error: 'ENOENT'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } var state = team.roster.getState() || {}; - cb(state.members || {}); + var members = state.members || {}; + + // Add online status (using messenger data) + var chatData = team.getChatData(); + var online = ctx.store.messenger.getOnlineList(chatData.channel) || []; + online.forEach(function (curve) { + if (members[curve]) { + members[curve].online = true; + } + }); + + cb(members); }; var getTeamMetadata = function (ctx, data, cId, cb) { @@ -635,13 +646,12 @@ define([ var state = team.roster.getState(); var userData = state.members[data.curvePublic]; - console.error(userData); team.roster.remove([data.curvePublic], function (err) { if (err) { return void cb({error: err}); } // The user has been removed, send them a notification if (!userData || !userData.notifications) { return cb(); } - console.log('send notif'); ctx.store.mailbox.sendTo('KICKED_FROM_TEAM', { + pending: data.pending, user: Messaging.createData(ctx.store.proxy, false), teamChannel: getInviteData(ctx, teamId).channel, teamName: getInviteData(ctx, teamId).metadata.name @@ -711,7 +721,10 @@ define([ var openTeamChat = function (ctx, data, cId, cb) { var team = ctx.teams[data.teamId]; if (!team) { return void cb({error: 'ENOENT'}); } - ctx.store.messenger.openTeamChat(team.getChatData(), cId, cb); + var onUpdate = function () { + ctx.emit('ROSTER_CHANGE', data.teamId, team.clients); + }; + ctx.store.messenger.openTeamChat(team.getChatData(), onUpdate, cId, cb); }; Team.init = function (cfg, waitFor, emit) { @@ -748,6 +761,7 @@ define([ var t = {}; Object.keys(teams).forEach(function (id) { t[id] = { + owner: teams[id].owner, name: teams[id].metadata.name, edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']), avatar: Util.find(teams[id], ['metadata', 'avatar']) @@ -775,6 +789,17 @@ define([ }); }; + team.updateMyData = function (data) { + Object.keys(ctx.teams).forEach(function (id) { + var team = ctx.teams[id]; + if (!team.roster) { return; } + var obj = {}; + obj[data.curvePublic] = data; + team.roster.describe(obj, function (err) { + if (err) { console.error(err); } + }); + }); + }; team.removeClient = function (clientId) { removeClient(ctx, clientId); }; diff --git a/www/poll/inner.js b/www/poll/inner.js index b9baff904..00c819491 100644 --- a/www/poll/inner.js +++ b/www/poll/inner.js @@ -821,7 +821,9 @@ define([ UI.errorLoadingScreen(errorText); throw new Error(errorText); } - } else { + } + if (!proxy.metadata || typeof(proxy.metadata.title) === "undefined") { + console.error("UPDATE TITLE"); Title.updateTitle(Title.defaultTitle); } diff --git a/www/teams/app-team.less b/www/teams/app-team.less index 8d7b30bd9..633482452 100644 --- a/www/teams/app-team.less +++ b/www/teams/app-team.less @@ -19,6 +19,16 @@ @roster-bg-color: #efefef; #cp-sidebarlayout-container { + @media screen and (max-width: 900px) { + .cp-app-drive-toolbar-leftside { + .cp-dropdown-button-title span:last-child { + display: none; + } + .cp-toolbar-share-button span:last-child { + display: none; + } + } + } div#cp-sidebarlayout-rightside.cp-rightside-drive { padding: 0; & > .cp-team-chat { @@ -39,8 +49,46 @@ } } - .cp-team-list-avatar { - .avatar_main(30px); + .cp-team-list { + .cp-team-list-container { + display: flex; + align-items: center; + justify-content: space-evenly; + flex-wrap: wrap; + } + .cp-team-list-team { + .tools_unselectable(); + background-color: @roster-bg-color; + display: flex; + align-items: center; + flex-flow: column; + width: 300px; + max-width: 90%; + height: 400px; + padding: 20px; + margin: 5px; + .cp-team-list-avatar { + .avatar_main(200px); + } + .cp-team-list-name { + flex: 1; + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 500px; + font-size: 25px; + display: inline-flex; + align-items: center; + &.empty { + white-space: initial; + text-align: center; + } + } + .cp-team-list-open { + width: 100%; + } + } } .cp-team-avatar { .avatar_main(300px); @@ -60,6 +108,16 @@ .cp-avatar { margin-right: 10px; } + .cp-team-member-status { + margin-left: 5px; + width: 5px; + height: 50px; + display: inline-block; + background-color: red; + &.online { + background-color: green; + } + } .cp-team-member-name { flex: 1; overflow: hidden; diff --git a/www/teams/inner.js b/www/teams/inner.js index 99853f415..95ae4735f 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -6,6 +6,7 @@ define([ '/common/common-interface.js', '/common/common-ui-elements.js', '/common/common-feedback.js', + '/common/common-constants.js', '/bower_components/nthen/index.js', '/common/sframe-common.js', '/common/proxy-manager.js', @@ -25,6 +26,7 @@ define([ UI, UIElements, Feedback, + Constants, nThen, SFCommon, ProxyManager, @@ -177,6 +179,8 @@ define([ }); if (active === 'drive') { APP.$rightside.addClass('cp-rightside-drive'); + } else { + APP.$rightside.removeClass('cp-rightside-drive'); } showCategories(categories[active]); }; @@ -264,25 +268,44 @@ define([ ]); }); + var MAX_TEAMS_SLOTS = Constants.MAX_TEAMS_SLOTS; var refreshList = function (common, cb) { var sframeChan = common.getSframeChannel(); var content = []; - content.push(h('h3', 'Your teams')); APP.module.execCommand('LIST_TEAMS', null, function (obj) { if (!obj) { return; } if (obj.error) { return void console.error(obj.error); } - var lis = []; - Object.keys(obj).forEach(function (id) { + var list = []; + var keys = Object.keys(obj).slice(0,3); + var slots = '('+Math.min(keys.length, MAX_TEAMS_SLOTS)+'/'+MAX_TEAMS_SLOTS+')'; + for (var i = keys.length; i < MAX_TEAMS_SLOTS; i++) { + obj[i] = { + empty: true + }; + keys.push(i); + } + + content.push(h('h3', Messages.team_listTitle + ' ' + slots)); + + keys.forEach(function (id) { var team = obj[id]; - var a = h('a', Messages.team_listLoad); - var avatar = h('span.cp-avatar.cp-team-list-avatar'); - lis.push(h('li', h('ul', [ // XXX UI - h('li', avatar), - h('li', team.metadata.name), - h('li', a) - ]))); + if (team.empty) { + list.push(h('div.cp-team-list-team.empty', [ + h('span.cp-team-list-name.empty', Messages.team_listSlot) + ])); + return; + } + var btn; + var avatar = h('span.cp-avatar'); + list.push(h('div.cp-team-list-team', [ + h('span.cp-team-list-avatar', avatar), + h('span.cp-team-list-name', { + title: team.metadata.name + }, team.metadata.name), + btn = h('button.cp-team-list-open.btn.btn-primary', Messages.team_listLoad) + ])); common.displayAvatar($(avatar), team.metadata.avatar, team.metadata.name); - $(a).click(function () { + $(btn).click(function () { APP.module.execCommand('SUBSCRIBE', id, function () { sframeChan.query('Q_SET_TEAM', id, function (err) { if (err) { return void console.error(err); } @@ -293,7 +316,7 @@ define([ }); }); }); - content.push(h('ul', lis)); + content.push(h('div.cp-team-list-container', list)); cb(content); }); return content; @@ -303,7 +326,20 @@ define([ }); makeBlock('create', function (common, cb) { + var metadataMgr = common.getMetadataMgr(); + var privateData = metadataMgr.getPrivateData(); var content = []; + + var isOwner = Object.keys(privateData.teams || {}).some(function (id) { + return privateData.teams[id].owner; + }); + if (Object.keys(privateData.teams || {}).length >= 3 || isOwner) { + content.push(h('div.alert.alert-warning', { + role:'alert' + }, isOwner ? Messages.team_maxOwner : Messages._getKey('team_maxTeams', [MAX_TEAMS_SLOTS]))); + return void cb(content); + } + content.push(h('h3', Messages.team_createLabel)); content.push(h('label', Messages.team_createName)); var input = h('input', {type:'text'}); @@ -393,6 +429,8 @@ define([ common.displayAvatar($(avatar), data.avatar, data.displayName); // Name var name = h('span.cp-team-member-name', data.displayName); + // Status + var status = h('span.cp-team-member-status'+(data.online ? '.online' : '')); // Actions var actions = h('span.cp-team-member-actions'); var $actions = $(actions); @@ -400,7 +438,7 @@ define([ var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1; 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) { + if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) { var promote = h('span.fa.fa-angle-double-up', { title: Messages.team_rosterPromote }); @@ -413,7 +451,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) { + if (!isMe && myRole >= theirRole && theirRole > 0 && !data.pending) { var demote = h('span.fa.fa-angle-double-down', { title: Messages.team_rosterDemote }); @@ -432,6 +470,7 @@ define([ $(remove).click(function () { $(remove).hide(); APP.module.execCommand('REMOVE_USER', { + pending: data.pending, teamId: APP.team, curvePublic: data.curvePublic, }, function (obj) { @@ -449,7 +488,8 @@ define([ var content = [ avatar, name, - actions + actions, + status, ]; var div = h('div.cp-team-roster-member', { title: data.displayName @@ -486,6 +526,12 @@ define([ }).map(function (k) { return makeMember(common, roster[k], me); }); + var pending = Object.keys(roster).filter(function (k) { + if (!roster[k].pending) { return; } + return roster[k].role === "MEMBER" || !roster[k].role; + }).map(function (k) { + return makeMember(common, roster[k], me); + }); var header = h('div.cp-app-team-roster-header'); var $header = $(header); @@ -538,7 +584,9 @@ define([ h('h3', Messages.team_admins), h('div', admins), h('h3', Messages.team_members), - h('div', members) + h('div', members), + h('h3', Messages.team_pending || 'PENDING'), // XXX + h('div', pending) ]; }; makeBlock('roster', function (common, cb) { @@ -576,6 +624,7 @@ define([ var todo = function () { var newName = $input.val(); + if (!newName.trim()) { return; } $spinner.show(); APP.module.execCommand('GET_TEAM_METADATA', { teamId: APP.team