define([ 'jquery', '/common/toolbar.js', '/common/drive-ui.js', '/common/common-util.js', '/common/common-hash.js', '/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', '/common/userObject.js', '/common/inner/common-mediatag.js', '/common/hyperscript.js', '/customize/application_config.js', '/common/messenger-ui.js', '/common/inner/invitation.js', '/common/make-backup.js', '/customize/messages.js', '/bower_components/file-saver/FileSaver.min.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/teams/app-team.less', ], function ( $, Toolbar, DriveUI, Util, Hash, UI, UIElements, Feedback, Constants, nThen, SFCommon, ProxyManager, UserObject, MT, h, AppConfig, MessengerUI, InviteInner, Backup, Messages) { var APP = { teams: {} }; var driveAPP = {}; var saveAs = window.saveAs; //var SHARED_FOLDER_NAME = Messages.fm_sharedFolderName; var copyObjectValue = function (objRef, objToCopy) { for (var k in objRef) { delete objRef[k]; } $.extend(true, objRef, objToCopy); }; var updateSharedFolders = function (sframeChan, manager, drive, folders, cb) { if (!drive || !drive.sharedFolders) { return void cb(); } var r = drive.restrictedFolders = {}; var oldIds = Object.keys(folders); nThen(function (waitFor) { Object.keys(drive.sharedFolders).forEach(function (fId) { var sfData = drive.sharedFolders[fId] || {}; var href = UserObject.getHref(sfData, APP.cryptor); var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); sframeChan.query('Q_DRIVE_GETOBJECT', { sharedFolder: fId }, waitFor(function (err, newObj) { if (newObj && newObj.restricted) { r[fId] = drive.sharedFolders[fId]; if (!r[fId].title) { r[fId].title = r[fId].lastTitle; } } if (newObj && (newObj.deprecated /*|| newObj.restricted*/)) { delete folders[fId]; delete drive.sharedFolders[fId]; if (manager && manager.folders) { delete manager.folders[fId]; } return; } folders[fId] = folders[fId] || {}; copyObjectValue(folders[fId], newObj); folders[fId].readOnly = !secret.keys.secondaryKey; if (manager && oldIds.indexOf(fId) === -1) { manager.addProxy(fId, { proxy: folders[fId] }, null, secret.keys.secondaryKey); } var readOnly = !secret.keys.editKeyStr; if (!manager || !manager.folders[fId]) { return; } manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey); manager.folders[fId].offline = newObj.offline; })); }); // Remove from memory folders that have been deleted from the drive remotely oldIds.forEach(function (fId) { if (!drive.sharedFolders[fId]) { delete folders[fId]; delete drive.sharedFolders[fId]; if (manager && manager.folders) { delete manager.folders[fId]; } } }); }).nThen(function () { cb(); }); }; var updateObject = function (sframeChan, obj, cb) { sframeChan.query('Q_DRIVE_GETOBJECT', null, function (err, newObj) { copyObjectValue(obj, newObj); cb(); }); }; var setEditable = DriveUI.setEditable; var closeTeam = function (common, cb) { var sframeChan = common.getSframeChannel(); APP.module.execCommand('SUBSCRIBE', null, function () { sframeChan.query('Q_SET_TEAM', null, function (err) { if (err) { return void console.error(err); } if (APP.drive && APP.drive.close) { APP.drive.close(); } $('.cp-toolbar-title-value').text(Messages.type.teams); sframeChan.event('EV_SET_TAB_TITLE', Messages.type.teams); APP.team = null; APP.teamEdPublic = null; APP.drive = null; APP.cryptor = null; APP.toolbar.$bottomR.empty(); APP.toolbar.$bottomM.empty(); APP.toolbar.$bottomL.empty(); APP.buildUI(common); if (APP.usageBar) { APP.usageBar.stop(); APP.usageBar = null; } if (cb) { cb(common); } }); }); }; var mainCategories = { 'list': [ // Msg.team_cat_list 'cp-team-list', ], 'create': [ 'cp-team-create', ], 'general': [ // Msg.team_cat_general 'cp-team-info', ], 'link': [ // Msg.team_cat_link 'cp-team-link', ], }; var teamCategories = { 'back': { // Msg.team_cat_back onClick: function (common) { closeTeam(common); } }, 'drive': [ // Msg.team_cat_drive 'cp-team-drive' ], 'members': [ // Msg.team_cat_members 'cp-team-offline', 'cp-team-roster' ], 'chat': [ // Msg.team_cat_chat 'cp-team-offline', 'cp-team-chat' ], 'admin': [ // Msg.team_cat_admin 'cp-team-offline', 'cp-team-edpublic', 'cp-team-name', 'cp-team-avatar', 'cp-team-export', 'cp-team-delete', ], }; var create = {}; // Sidebar layout var hideCategories = function () { APP.$rightside.find('> div').hide(); }; var showCategories = function (cat) { hideCategories(); cat.forEach(function (c) { APP.$rightside.find('.'+c).css('display', ''); }); }; var createLeftSide = APP.createLeftSide = function (common, team, teamAdmin) { APP.$leftside.empty(); var $categories = $('<div>', {'class': 'cp-sidebarlayout-categories'}) .appendTo(APP.$leftside); var hash = common.getMetadataMgr().getPrivateData().teamInviteHash && mainCategories.link; var categories = team ? teamCategories : mainCategories; var active = team ? 'drive' : (hash ? 'link' : 'list'); if (team && APP.team) { var $category = $('<div>', {'class': 'cp-sidebarlayout-category cp-team-cat-header'}).appendTo($categories); var avatar = h('div.cp-avatar'); var $avatar = $(avatar); APP.module.execCommand('GET_TEAM_METADATA', { teamId: APP.team }, function (obj) { if (obj && obj.error) { return void UI.warn(Messages.error); } // Refresh offline state APP.teams[APP.team] = APP.teams[APP.team] || {}; APP.teams[APP.team].offline = obj.offline; common.displayAvatar($avatar, obj.avatar, obj.name); $category.append($avatar); $avatar.append(h('span.cp-sidebarlayout-category-name', obj.name)); }); } Object.keys(categories).forEach(function (key) { if (key === 'admin' && !teamAdmin) { return; } var $category = $('<div>', {'class': 'cp-sidebarlayout-category cp-team-cat-'+key}).appendTo($categories); if (key === 'general') { $category.append($('<span>', {'class': 'fa fa-info-circle'})); } if (key === 'list') { $category.append($('<span>', {'class': 'fa fa-list cp-team-cat-list'})); } if (key === 'create') { $category.append($('<span>', {'class': 'fa fa-plus-circle'})); } if (key === 'back') { $category.append($('<span>', {'class': 'fa fa-arrow-left'})); } if (key === 'members') { $category.append($('<span>', {'class': 'fa fa-users'})); } if (key === 'chat') { $category.append($('<span>', {'class': 'fa fa-comments'})); } if (key === 'drive') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); } if (key === 'admin') { $category.append($('<span>', {'class': 'fa fa-cogs'})); } if (key === 'link') { $category.append($('<span>', {'class': 'fa fa-envelope'})); } if (key === active) { $category.addClass('cp-leftside-active'); } $category.click(function () { if (!Array.isArray(categories[key]) && categories[key].onClick) { categories[key].onClick(common); return; } if (active === key) { return; } active = key; if (key === 'drive' || key === 'chat') { APP.$rightside.addClass('cp-rightside-drive'); APP.$leftside.addClass('cp-leftside-narrow'); } else { APP.$rightside.removeClass('cp-rightside-drive'); APP.$leftside.removeClass('cp-leftside-narrow'); } if (key === 'chat') { $category.find('.cp-team-chat-notification').removeClass('cp-team-chat-notification'); } $categories.find('.cp-leftside-active').removeClass('cp-leftside-active'); $category.addClass('cp-leftside-active'); showCategories(categories[key]); }); $category.append(h('span.cp-sidebarlayout-category-name', Messages['team_cat_'+key] || key)); }); if (active === 'drive') { APP.$rightside.addClass('cp-rightside-drive'); APP.$leftside.addClass('cp-leftside-narrow'); } else { APP.$rightside.removeClass('cp-rightside-drive'); APP.$leftside.removeClass('cp-leftside-narrow'); } showCategories(categories[active]); }; var buildUI = APP.buildUI = function (common, team, teamAdmin) { var $rightside = APP.$rightside; $rightside.empty(); var added = []; var addItem = function (cssClass) { var item = cssClass.slice(8); if (typeof (create[item]) === "function" && added.indexOf(item) < 0) { $rightside.append(create[item](common)); added.push(item); } }; var categories = team ? teamCategories : mainCategories; for (var cat in categories) { if (!Array.isArray(categories[cat])) { continue; } categories[cat].forEach(addItem); } createLeftSide(common, team, teamAdmin); }; // Team APP var loadTeam = function (common, id) { var metadataMgr = common.getMetadataMgr(); var privateData = metadataMgr.getPrivateData(); var sframeChan = common.getSframeChannel(); var proxy = {}; var folders = {}; nThen(function (waitFor) { updateObject(sframeChan, proxy, waitFor(function () { updateSharedFolders(sframeChan, null, proxy.drive, folders, waitFor()); })); }).nThen(function () { if (!proxy.drive || typeof(proxy.drive) !== 'object') { throw new Error("Corrupted drive"); } driveAPP.team = id; // Provide secondaryKey var teamData = (privateData.teams || {})[id] || {}; driveAPP.readOnly = !teamData.hasSecondaryKey; if (APP.usageBar) { APP.usageBar.stop(); } APP.usageBar = undefined; if (!driveAPP.readOnly) { APP.usageBar = common.createUsageBar(APP.team, function (err, $limitContainer) { if (err) { return void DriveUI.logError(err); } $limitContainer.attr('title', Messages.team_quota); }, true); } driveAPP.online = !teamData.offline; var drive = DriveUI.create(common, { proxy: proxy, folders: folders, updateObject: updateObject, updateSharedFolders: updateSharedFolders, $limit: APP.usageBar && APP.usageBar.$container, toolbar: APP.toolbar, APP: driveAPP, edPublic: APP.teamEdPublic, editKey: teamData.secondaryKey }); APP.drive = drive; driveAPP.refresh = drive.refresh; if (APP.teams[id] && APP.teams[id].offline) { setEditable(false); drive.refresh(); } }); }; var loadMain = function (common) { buildUI(common); UI.removeLoadingScreen(); }; // Rightside elements var makeBlock = function (key, getter, full) { var safeKey = key.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); create[key] = function (common) { var $div = $('<div>', {'class': 'cp-team-' + key + ' cp-sidebarlayout-element'}); if (full) { $('<label>').text(Messages['team_'+safeKey+'Title'] || key).appendTo($div); $('<span>', {'class': 'cp-sidebarlayout-description'}) .text(Messages['team_'+safeKey+'Hint'] || 'Coming soon...').appendTo($div); } getter(common, function (content) { $div.append(content); }, $div); return $div; }; }; makeBlock('info', function (common, cb) { cb([ h('h3', Messages.team_infoLabel), h('p', Messages.team_infoContent) ]); }); var MAX_TEAMS_SLOTS = Constants.MAX_TEAMS_SLOTS; var openTeam = function (common, id, team) { var sframeChan = common.getSframeChannel(); APP.module.execCommand('SUBSCRIBE', id, function () { var t = Messages._getKey('team_title', [Util.fixHTML(team.metadata.name)]); sframeChan.query('Q_SET_TEAM', id, function (err) { if (err) { return void console.error(err); } // Set editable var metadataMgr = common.getMetadataMgr(); var privateData = metadataMgr.getPrivateData(); if (team.offline) { APP.$body.addClass('cp-app-team-offline'); } else if (!privateData.offline) { APP.$body.removeClass('cp-app-team-offline'); } // Change title $('.cp-toolbar-title-value').text(t); sframeChan.event('EV_SET_TAB_TITLE', t); // Get secondary key var secret = Hash.getSecrets('team', team.hash || team.roHash, team.password); APP.cryptor = UserObject.createCryptor(secret.keys.secondaryKey); // Load data APP.team = id; APP.teamEdPublic = Util.find(team, ['keys', 'drive', 'edPublic']); buildUI(common, true, team.owner); }); }); }; var canCreateTeams = function (teams) { var owned = Object.keys(teams || {}).filter(function (id) { return teams[id].owner; }).length; return Constants.MAX_TEAMS_OWNED - owned; }; var refreshList = function (common, cb) { var content = []; APP.module.execCommand('LIST_TEAMS', null, function (obj) { if (!obj) { return; } if (obj.error === "OFFLINE") { return UI.alert(Messages.driveOfflineError); } if (obj.error) { return void console.error(obj.error); } var list = []; var keys = Object.keys(obj).slice(0,MAX_TEAMS_SLOTS); var slots = '('+Math.min(keys.length, MAX_TEAMS_SLOTS)+'/'+MAX_TEAMS_SLOTS+')'; var createSlots = canCreateTeams(obj); 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)); APP.teams = {}; var created = 0; keys.forEach(function (id) { if (!obj[id].empty) { APP.teams[id] = { offline: obj[id] && obj[id].offline }; } var team = obj[id]; var createBtn; var createCls = ''; if (team.empty && created < createSlots) { createBtn = h('div.cp-team-list-team-create', [ h('i.fa.fa-plus-circle'), h('span', Messages.team_cat_create) ]); createCls = '.create'; created++; } if (team.empty) { var createTeamDiv = h('div.cp-team-list-team.empty'+createCls, [ h('span.cp-team-list-name.empty', Messages.team_listSlot), createBtn ]); list.push(createTeamDiv); if (createCls) { $(createTeamDiv).click(function () { $('div.cp-team-cat-create').click(); }); } return; } var avatar = h('span.cp-avatar'); var teamDiv = 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), ]); list.push(teamDiv); common.displayAvatar($(avatar), team.metadata.avatar, team.metadata.name); $(teamDiv).click(function () { if (team.error) { UI.warn(Messages.error); // FIXME better error message - roster bug, can't load the team for now return; } openTeam(common, id, team); }); }); content.push(h('div.cp-team-list-container', list)); cb(content); }); return content; }; makeBlock('list', function (common, cb) { refreshList(common, cb); }); var refreshLink = function () {}; // placeholder var refreshCreate = function (common, cb) { var metadataMgr = common.getMetadataMgr(); var privateData = metadataMgr.getPrivateData(); var content = []; var isOwner = Object.keys(privateData.teams || {}).filter(function (id) { return privateData.teams[id].owner; }).length >= Constants.MAX_TEAMS_OWNED && !privateData.devMode; var getWarningBox = function () { return h('div.alert.alert-warning', { role:'alert' }, Messages._getKey('team_maxTeams', [MAX_TEAMS_SLOTS])); }; if (Object.keys(privateData.teams || {}).length >= Constants.MAX_TEAMS_SLOTS || isOwner) { content.push(getWarningBox()); return void cb(content); } content.push(h('h3', Messages.team_createLabel)); content.push(h('label', Messages.team_createName)); var input = h('input', {type:'text'}); content.push(input); var button = h('button.btn.btn-success', Messages.creation_create); content.push(h('br')); content.push(h('br')); content.push(button); var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide(); content.push($spinner[0]); var state = false; $(button).click(function () { if (state) { return; } var name = $(input).val(); if (!name.trim()) { return; } state = true; $spinner.show(); APP.module.execCommand('CREATE_TEAM', { name: name }, function (obj) { if (obj && obj.error) { $spinner.hide(); state = false; if (obj.error === "OFFLINE") { return UI.warn(Messages.disconnected); } console.error(obj.error); return void UI.warn(Messages.error); } // Redraw the create block var $createDiv = $('div.cp-team-create').empty(); isOwner = true; $createDiv.append(getWarningBox()); // Redraw the teams list var $div = $('div.cp-team-list').empty(); refreshList(common, function (content) { state = false; $div.append(content); $spinner.hide(); $('div.cp-team-cat-list').click(); }); var $divLink = $('div.cp-team-link').empty(); if ($divLink.length) { refreshLink(common, function (content) { $divLink.append(content); }); } }); }); cb(content); }; makeBlock('create', function (common, cb) { refreshCreate(common, cb); }); makeBlock('drive', function (common, cb, $div) { $('div.cp-team-drive').empty(); $div.removeClass('cp-sidebarlayout-element'); // Don't apply buttons and input styles from sidebarlayout var content = [ h('div.cp-app-drive-container', {tabindex:0}, [ h('div#cp-app-drive-tree'), h('div#cp-app-drive-content-container', [ h('div#cp-app-drive-connection-state.cp-banner.cp-banner-danger', {style: "display: none;"}, Messages.disconnected), h('div#cp-app-drive-content', {tabindex:2}) ]) ]) ]; UI.addLoadingScreen(); cb(content); loadTeam(common, APP.team, false); }); var redrawRoster = function (common, _$roster) { var $roster = _$roster || $('#cp-team-roster-container'); if (!$roster.length) { return; } APP.module.execCommand('GET_TEAM_ROSTER', { teamId: APP.team }, function (obj) { if (obj && obj.error) { return void UI.warn(Messages.error); } var roster = APP.refreshRoster(common, obj); $roster.empty().append(roster); }); }; var makePermissions = function () { var modal= UI.createModal({ id: 'cp-teams-roster-dialog', }); modal.show(); var $blockContainer = modal.$modal; var makeRow = function (arr, first) { return arr.map(function (val) { return h(first ? 'th' : 'td', val); }); }; // Global rights var rows = []; var firstRow = [Messages.teams_table_role, Messages.share_linkView, Messages.share_linkEdit, Messages.teams_table_admins, Messages.teams_table_owners]; rows.push(h('tr', makeRow(firstRow, true))); rows.push(h('tr', makeRow([ Messages.team_viewers, h('span.fa.fa-check'), h('span.fa.fa-times'), h('span.fa.fa-times'), h('span.fa.fa-times') ]))); rows.push(h('tr', makeRow([ Messages.team_members, h('span.fa.fa-check'), h('span.fa.fa-check'), h('span.fa.fa-times'), h('span.fa.fa-times') ]))); rows.push(h('tr', makeRow([ Messages.team_admins, h('span.fa.fa-check'), h('span.fa.fa-check'), h('span.fa.fa-check'), h('span.fa.fa-times') ]))); rows.push(h('tr', makeRow([ Messages.team_owner, h('span.fa.fa-check'), h('span.fa.fa-check'), h('span.fa.fa-check'), h('span.fa.fa-check') ]))); var t = h('table.cp-teams-generic', rows); var content = [ h('h4', Messages.teams_table_generic), h('p', [ Messages.teams_table_generic_view, h('br'), Messages.teams_table_generic_edit, h('br'), Messages.teams_table_generic_admin, h('br'), Messages.teams_table_generic_own, h('br') ]), t ]; APP.module.execCommand('GET_EDITABLE_FOLDERS', { teamId: APP.team }, function (arr) { if (!Array.isArray(arr) || !arr.length) { return void $blockContainer.find('.cp-modal').append(content); } content.push(h('h5', Messages.teams_table_specific)); content.push(h('p', Messages.teams_table_specificHint)); var paths = arr.map(function (obj) { obj.path.push(obj.name); return h('li', obj.path.join('/')); }); content.push(h('ul', paths)); /* var rows = []; rows.push(h('tr', makeRow(firstRow, true))); rows.push(h('tr', makeRow([Messages.team_viewers, , , '', '']))); content.push(h('table', rows)); */ $blockContainer.find('.cp-modal').append(content); }); }; var ROLES = ['VIEWER', 'MEMBER', 'ADMIN', 'OWNER']; var describeUser = function (common, curvePublic, data, icon) { APP.module.execCommand('DESCRIBE_USER', { teamId: APP.team, curvePublic: curvePublic, data: data }, function (obj) { if (obj && obj.error) { $(icon).show(); return void UI.alert(Messages.error); } redrawRoster(common); }); }; var getDisplayName = UI.getDisplayName; var makeMember = function (common, data, me, roster) { if (!data.curvePublic) { return; } var otherOwners = Object.keys(roster || {}).some(function (key) { var user = roster[key]; return user.role === "OWNER" && user.curvePublic !== me.curvePublic && !user.pendingOwner; }); var displayName = getDisplayName(data.displayName); // Avatar var avatar = h('span.cp-avatar.cp-team-member-avatar'); common.displayAvatar($(avatar), data.avatar, displayName, Util.noop, data.uid); // Name var name = h('span.cp-team-member-name', displayName); if (data.pendingOwner) { $(name).append(h('em', { title: Messages.team_pendingOwnerTitle }, ' ' + Messages.team_pendingOwner)); } // Status var status = h('span.cp-team-member-status'+(data.online ? '.online' : '')); // Actions var actions = h('span.cp-online.cp-team-member-actions'); var $actions = $(actions); var isMe = me && me.curvePublic === data.curvePublic; var myRole = me ? (ROLES.indexOf(me.role) || 1) : -1; var theirRole = ROLES.indexOf(data.role); var ADMIN = ROLES.indexOf('ADMIN'); // If they're an admin and I am an owner, I can promote them to owner if (!isMe && myRole > theirRole && theirRole === ADMIN && !data.pending) { var promoteOwner = h('span.fa.fa-angle-double-up', { title: Messages.team_rosterPromoteOwner }); $(promoteOwner).click(function () { UI.confirm(Messages.team_ownerConfirm, function (yes) { if (!yes) { return; } $(promoteOwner).hide(); APP.module.execCommand('OFFER_OWNERSHIP', { teamId: APP.team, curvePublic: data.curvePublic }, function (obj) { if (obj && obj.error) { console.error(obj.error); return void UI.warn(Messages.error); } UI.log(Messages.sent); }); }); }); $actions.append(promoteOwner); } // If they're a viewer/member and I have a higher role than them, I can promote them to admin if (!isMe && myRole >= ADMIN && theirRole < ADMIN && !data.pending) { var promote = h('span.fa.fa-angle-double-up', { title: Messages.team_rosterPromote }); $(promote).click(function () { $(promote).hide(); describeUser(common, data.curvePublic, { role: ROLES[theirRole + 1] }, promote); }); $actions.append(promote); } // 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 (myRole >= theirRole && myRole >= ADMIN && theirRole > 0 && !data.pending) { var demote = h('span.fa.fa-angle-double-down', { title: Messages.team_rosterDemote }); $(demote).click(function () { var todo = function () { var role = ROLES[theirRole - 1] || 'VIEWER'; $(demote).hide(); describeUser(common, data.curvePublic, { role: role }, demote); }; if (isMe) { return void UI.confirm(Messages.team_demoteMeConfirm, function (yes) { if (!yes) { return; } todo(); }); } todo(); }); if (!(isMe && myRole === 3 && !otherOwners)) { $actions.append(demote); } } // If I'm at least an admin and I have an equal or higher role than them, I can remove them // Note: we can't remove owners, we have to demote them first if (!isMe && myRole >= ADMIN && myRole >= theirRole && theirRole !== ROLES.indexOf('OWNER')) { var remove = h('span.fa.fa-times', { title: Messages.team_rosterKick }); $(remove).click(function () { UI.confirm(Messages._getKey('team_kickConfirm', [Util.fixHTML(displayName)]), function (yes) { if (!yes) { return; } APP.module.execCommand('REMOVE_USER', { pending: data.pending, teamId: APP.team, curvePublic: data.curvePublic, }, function (obj) { if (obj && obj.error) { $(remove).show(); return void UI.alert(Messages.error); } redrawRoster(common); }); }); }); $actions.append(remove); } // User var content = [ avatar, name, actions, status, ]; var div = h('div.cp-team-roster-member', content); if (data.profile) { $(div).dblclick(function (e) { e.preventDefault(); e.stopPropagation(); common.openURL('/profile/#' + data.profile); }); } return div; }; APP.refreshRoster = function (common, roster) { if (!roster || typeof(roster) !== "object" || Object.keys(roster) === 0) { return; } var metadataMgr = common.getMetadataMgr(); var userData = metadataMgr.getUserData(); var me = roster[userData.curvePublic] || {}; var owner = Object.keys(roster).filter(function (k) { if (roster[k].pending) { return; } roster[k].curvePublic = k; return roster[k].role === "OWNER" || roster[k].pendingOwner; }).map(function (k) { return makeMember(common, roster[k], me, roster); }); var admins = Object.keys(roster).filter(function (k) { if (roster[k].pending) { return; } roster[k].curvePublic = k; return roster[k].role === "ADMIN"; }).map(function (k) { return makeMember(common, roster[k], me); }); var members = Object.keys(roster).filter(function (k) { if (roster[k].pending) { return; } roster[k].curvePublic = k; return roster[k].role === "MEMBER" || !roster[k].role; }).map(function (k) { return makeMember(common, roster[k], me); }); var viewers = Object.keys(roster).filter(function (k) { if (roster[k].pending) { return; } roster[k].curvePublic = k; return roster[k].role === "VIEWER"; }).map(function (k) { return makeMember(common, roster[k], me); }); var pending = Object.keys(roster).filter(function (k) { if (!roster[k].pending) { return; } if (roster[k].inviteChannel) { return; } roster[k].curvePublic = k; return roster[k].role === "MEMBER" || roster[k].role === "VIEWER" || !roster[k].role; }).map(function (k) { return makeMember(common, roster[k], me); }); var links = Object.keys(roster).filter(function (k) { if (!roster[k].pending) { return; } if (!roster[k].inviteChannel) { return; } roster[k].curvePublic = k; return roster[k].role === "VIEWER" || !roster[k].role; }).map(function (k) { return makeMember(common, roster[k], me); }); 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.cp-online.btn.btn-primary', Messages.team_inviteButton); 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); } var leave = h('button.cp-online.btn.btn-danger', Messages.team_leaveButton); $(leave).click(function () { if (me && me.role === 'OWNER') { return void UI.alert(Messages.team_leaveOwner); } UI.confirm(Messages.team_leaveConfirm, function (yes) { if (!yes) { return; } APP.module.execCommand('LEAVE_TEAM', { teamId: APP.team }, function (obj) { if (obj && obj.error) { return void UI.warn(Messages.error); } }); }); }); $header.append(leave); var table = h('button.btn.btn-primary', Messages.teams_table); $(table).click(function (e) { e.stopPropagation(); makePermissions(); }); $header.append(table); var noPending = pending.length ? '' : '.cp-hidden'; var noLinks = links.length ? '' : '.cp-hidden'; return [ header, h('h3', Messages.team_owner), h('div', owner), h('h3', Messages.team_admins), h('div', admins), h('h3', Messages.team_members), h('div', members), h('h3', Messages.team_viewers || 'VIEWERS'), h('div', viewers), h('h3'+noPending, Messages.team_pending), h('div'+noPending, pending), h('h3'+noLinks, Messages.team_links), h('div'+noLinks, links) ]; }; makeBlock('roster', function (common, cb) { var container = h('div#cp-team-roster-container'); var content = [container]; redrawRoster(common, $(container)); cb(content); }); makeBlock('offline', function (common, cb, $div) { $div.addClass('cp-offline'); cb(h('div.cp-banner.cp-banner-danger', Messages.disconnected)); }); makeBlock('chat', function (common, cb, $div) { $div.addClass('cp-online'); var container = h('div#cp-app-contacts-container.cp-app-contacts-inapp'); var content = [container]; APP.module.execCommand('OPEN_TEAM_CHAT', { teamId: APP.team }, function (obj) { if (obj && obj.error) { if (obj.error === 'OFFLINE') { return; } return void UI.alert(Messages.error); } common.setTeamChat(obj.channel); MessengerUI.create($(container), common, { chat: $('.cp-team-cat-chat'), team: true, readOnly: obj.readOnly }); cb(content); }); }); makeBlock('edpublic', function (common, cb) { var container = h('div'); var $div = $(container); var metadataMgr = common.getMetadataMgr(); var privateData = metadataMgr.getPrivateData(); var team = privateData.teams[APP.team]; if (!team) { return void cb(); } var publicKey = team.edPublic; var name = team.name; if (publicKey) { var $key = $('<div>', {'class': 'cp-sidebarlayout-element'}).appendTo($div); var userHref = Hash.getPublicSigningKeyString(privateData.origin, name, publicKey); var $pubLabel = $('<span>', {'class': 'label'}) .text(Messages.settings_publicSigningKey); $key.append($pubLabel).append(UI.dialog.selectable(userHref)); } var content = [container]; cb(content); }); makeBlock('name', function (common, cb) { // Msg.team_nameHint, .team_nameTitle var $inputBlock = $('<div>', {'class': 'cp-sidebarlayout-input-block'}); var $input = $('<input>', { 'type': 'text', 'id': 'cp-settings-displayname', 'placeholder': Messages.anonymous}).appendTo($inputBlock); var $save = $('<button>', {'class': 'cp-online-alt btn btn-primary'}).text(Messages.settings_save).appendTo($inputBlock); var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide(); var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide(); var todo = function () { var newName = $input.val(); if (!newName.trim()) { return; } $spinner.show(); APP.module.execCommand('GET_TEAM_METADATA', { teamId: APP.team }, function (obj) { if (obj && obj.error) { return void UI.warn(Messages.error); } var oldName = obj.name; obj.name = newName; APP.module.execCommand('SET_TEAM_METADATA', { teamId: APP.team, metadata: obj }, function (res) { $spinner.hide(); if (res && res.error) { $input.val(oldName); if (res.error === 'OFFLINE') { return void UI.warn(Messages.disconnected); } return void UI.warn(Messages.error); } $ok.show(); }); }); }; APP.module.execCommand('GET_TEAM_METADATA', { teamId: APP.team }, function (obj) { if (obj && obj.error) { return void UI.warn(Messages.error); } $input.val(obj.name); $input.on('keyup', function (e) { if ($input.val() !== obj.name) { $ok.hide(); } if (e.which === 13) { todo(); } }); $save.click(todo); var content = [ $inputBlock[0], $ok[0], $spinner[0] ]; cb(content); }); }, true); makeBlock('avatar', function (common, cb) { // Msg.team_avatarHint, .team_avatarTitle // Upload var avatar = h('div.cp-team-avatar.cp-avatar'); var $avatar = $(avatar); var data = MT.addAvatar(common, function (ev, data) { if (!data.url) { return void UI.warn(Messages.error); } APP.module.execCommand('GET_TEAM_METADATA', { teamId: APP.team }, function (obj) { if (obj && obj.error) { return void UI.warn(Messages.error); } obj.avatar = data.url; APP.module.execCommand('SET_TEAM_METADATA', { teamId: APP.team, metadata: obj }, function () { $avatar.empty(); // the UI is not supposed to allow admins to remove team names // so we expect that it will be there. Failing that the initials // from the default name will be displayed common.displayAvatar($avatar, data.url); }); }); }); var $upButton = common.createButton('upload', false, data); $upButton.addClass('cp-online'); $upButton.removeProp('title'); $upButton.text(Messages.profile_upload); $upButton.prepend($('<span>', {'class': 'fa fa-upload'})); APP.module.execCommand('GET_TEAM_METADATA', { teamId: APP.team }, function (obj) { if (obj && obj.error) { return void UI.warn(Messages.error); } var val = obj.avatar; if (!val) { var $img = $('<img>', { src: '/customize/images/avatar.png', title: Messages.profile_defaultAlt, alt: Messages.profile_defaultAlt, }); var mt = h('media-tag', $img[0]); $avatar.append(mt); } else { common.displayAvatar($avatar, val); } // Display existing + button var content = [ avatar, h('br'), $upButton[0] ]; cb(content); }); }, true); makeBlock('export', function (common, cb) { // Msg.team_exportHint, .team_exportTitle // Backup all the pads var sframeChan = common.getSframeChannel(); var privateData = common.getMetadataMgr().getPrivateData(); var team = privateData.teams[APP.team] || {}; var teamName = team.name || Messages.anonymous; var exportDrive = function() { Feedback.send('FULL_TEAMDRIVE_EXPORT_START'); var todo = function(data, filename) { var ui = Backup.createExportUI(privateData.origin); var bu = Backup.create(data, common.getPad, privateData.fileHost, function(blob, errors) { saveAs(blob, filename); sframeChan.event('EV_CRYPTGET_DISCONNECT'); ui.complete(function() { Feedback.send('FULL_TEAMDRIVE_EXPORT_COMPLETE'); saveAs(blob, filename); }, errors); }, ui.update, common.getCache, common.getSframeChannel()); ui.onCancel(function() { ui.close(); bu.stop(); }); }; sframeChan.query("Q_SETTINGS_DRIVE_GET", "full", function(err, data) { if (err) { return void console.error(err); } if (data.error) { return void console.error(data.error); } var filename = teamName + '-' + new Date().toDateString() + '.zip'; todo(data, filename); }); }; var button = h('button.btn.btn-primary', Messages.team_exportButton); UI.confirmButton(button, { classes: 'btn-primary', multiple: true }, function () { exportDrive(); }); cb(button); }, true); makeBlock('delete', function (common, cb, $div) { // Msg.team_deleteHint, .team_deleteTitle $div.addClass('cp-online'); var deleteTeam = h('button.btn.btn-danger', Messages.team_deleteButton); var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide(); var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide(); var deleting = false; $(deleteTeam).click(function () { if (deleting) { return; } UI.confirm(Messages.team_deleteConfirm, function (yes) { if (!yes) { return; } if (deleting) { return; } deleting = true; $spinner.show(); APP.module.execCommand("DELETE_TEAM", { teamId: APP.team }, function (obj) { $spinner.hide(); deleting = false; if (obj && obj.error) { return void UI.warn(obj.error); } $ok.show(); UI.log(Messages.deleted); }); }); }); cb([ deleteTeam, $ok[0], $spinner[0] ]); }, true); var displayUser = function (common, data) { var avatar = h('span.cp-teams-invite-from-avatar.cp-avatar'); var name = getDisplayName(data.displayName); common.displayAvatar($(avatar), data.avatar, name); return h('div.cp-teams-invite-from-author', [ avatar, h('span.cp-teams-invite-from-name', name) ]); }; refreshLink = function (common, cb, wrongPassword) { if (!mainCategories.link) { return; } var privateData = common.getMetadataMgr().getPrivateData(); var hash = privateData.teamInviteHash; var hashData = Hash.parseTypeHash('invite', hash); var password = hashData.password; var seeds = InviteInner.deriveSeeds(hashData.key); var sframeChan = common.getSframeChannel(); if (Object.keys(privateData.teams || {}).length >= Constants.MAX_TEAMS_SLOTS) { return void cb([ h('div.alert.alert-danger', { role: 'alert' }, Messages._getKey('team_maxTeams', [Constants.MAX_TEAMS_SLOTS])) ]); } var div = h('div', [ h('i.fa.fa-spin.fa-spinner') ]); var $div = $(div); var errorBlock; var c = [ h('h2', Messages.team_inviteTitle), errorBlock = h('div.alert.alert-danger', wrongPassword ? undefined : {style: 'display: none;'}, wrongPassword ? Messages.drive_sfPasswordError : undefined), div ]; // "cb" will put the content into the UI. // We're displaying a spinner while we're cryptgetting the preview content cb(c); var declineButton = h('button.btn.btn-danger', { style: 'display: none;' }, Messages.friendRequest_decline); var acceptButton = h('button.btn.btn-primary', Messages.team_inviteJoin); var inviteDiv = h('div', [ h('nav', [ declineButton, acceptButton ]) ]); var $inviteDiv = $(inviteDiv); $(declineButton).click(function() { }); var process = function (pw) { $inviteDiv.empty(); var bytes64; var spinnerText; var $spinner; nThen(function (waitFor) { $inviteDiv.append(h('div', [ h('i.fa.fa-spin.fa-spinner'), spinnerText = h('span', Messages.team_invitePasswordLoading || 'Scrypt...') ])); $spinner = $(spinnerText); setTimeout(waitFor(), 150); }).nThen(function (waitFor) { var salt = InviteInner.deriveSalt(pw, AppConfig.loginSalt); InviteInner.deriveBytes(seeds.scrypt, salt, waitFor(function (bytes) { bytes64 = bytes; })); }).nThen(function (waitFor) { $spinner.text(Messages.team_inviteGetData); APP.module.execCommand('ACCEPT_LINK_INVITATION', { bytes64: bytes64, hash: hash, password: pw, }, waitFor(function (obj) { if (obj && obj.error) { console.error(obj.error); // Wrong password or other error... waitFor.abort(); if (obj.error === 'INVALID_INVITE_CONTENT') { // Wrong password... var $divLink = $('div.cp-team-link').empty(); if ($divLink.length) { refreshLink(common, function (content) { $divLink.append(content); }, true); } return; } $(errorBlock).text(Messages.team_inviteInvalidLinkError).show(); $(div).empty(); $inviteDiv.empty(); return; } // No error: join successful! sframeChan.event('EV_SET_HASH', ''); var $div = $('div.cp-team-list').empty(); refreshList(common, function (content) { $div.append(content); $('div.cp-team-cat-list').click(); var $divLink = $('div.cp-team-link').empty(); if ($divLink.length) { $divLink.remove(); $('div.cp-team-cat-link').remove(); delete mainCategories.link; } }); var $divCreate = $('div.cp-team-create'); if ($divCreate.length) { refreshCreate(common, function (content) { $divCreate.empty().append(content); }); } })); }); }; var isValidInvitationLinkContent = function (json) { if (!json) { return false; } if (json.error || !Object.keys(json).length) { return false; } if (!json.author) { return false; } return true; }; nThen(function (waitFor) { // Get preview content. sframeChan.query('Q_ANON_GET_PREVIEW_CONTENT', { seeds: seeds }, waitFor(function (err, json) { if (!isValidInvitationLinkContent(json)) { $(errorBlock).text(Messages.team_inviteInvalidLinkError).show(); waitFor.abort(); $div.empty(); return; } // FIXME nothing guarantees that teamName or author.displayName exist in json $div.empty(); $div.append(h('div.cp-teams-invite-from', [ Messages.team_inviteFrom, displayUser(common, json.author) ])); $div.append(UI.setHTML(h('p.cp-teams-invite-to'), Messages._getKey('team_inviteFromMsg', [Util.fixHTML(getDisplayName(json.author.displayName)), Util.fixHTML(json.teamName)]))); if (json.message) { $div.append(h('div.cp-teams-invite-message', json.message)); } })); }).nThen(function (waitFor) { // If you're logged in, move on to the next nThen if (driveAPP.loggedIn) { return; } // If you're not logged in, display the login buttons var anonLogin, anonRegister; $div.append(h('p', Messages.team_invitePleaseLogin)); $div.append(h('div', [ anonLogin = h('button.btn.btn-primary', Messages.login_login), anonRegister = h('button.btn.btn-secondary', Messages.login_register), ])); $(anonLogin).click(function () { common.setLoginRedirect('login'); }); $(anonRegister).click(function () { common.setLoginRedirect('register'); }); waitFor.abort(); }).nThen(function () { $div.append($inviteDiv); }).nThen(function (waitFor) { // If there is no password, move on to the next block if (!password) { return; } // If there is a password, display the password prompt var pwInput = UI.passwordInput(); $(acceptButton).click(function () { var val = $(pwInput).find('input').val(); if (!val) { return; } process(val); }); $inviteDiv.prepend(h('div.cp-teams-invite-password', [ h('p', Messages.team_inviteEnterPassword), pwInput ])); waitFor.abort(); }).nThen(function () { // No password, display the invitation proposal $(acceptButton).click(function () { process(''); }); }); return c; }; makeBlock('link', function (common, cb) { refreshLink(common, cb); }); var redrawTeam = function (common) { if (!APP.team) { return; } var teamId = APP.team; APP.module.execCommand('LIST_TEAMS', null, function (obj) { if (!obj) { return; } if (obj.error) { return void console.error(obj.error); } var team = obj[teamId]; if (!team) { return; } closeTeam(common, function () { openTeam(common, teamId, team); }); }); }; var main = function () { var common; var readOnly; nThen(function (waitFor) { $(waitFor(function () { UI.addLoadingScreen(); })); window.cryptpadStore.getAll(waitFor(function (val) { driveAPP.store = JSON.parse(JSON.stringify(val)); })); SFCommon.create(waitFor(function (c) { common = c; })); }).nThen(function (waitFor) { APP.$container = $('#cp-sidebarlayout-container'); APP.$leftside = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container); APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container); var sFrameChan = common.getSframeChannel(); sFrameChan.onReady(waitFor()); }).nThen(function () { var sframeChan = common.getSframeChannel(); var metadataMgr = common.getMetadataMgr(); var privateData = metadataMgr.getPrivateData(); var user = metadataMgr.getUserData(); readOnly = driveAPP.readOnly = metadataMgr.getPrivateData().readOnly; driveAPP.loggedIn = common.isLoggedIn(); //if (!driveAPP.loggedIn) { throw new Error('NOT_LOGGED_IN'); } common.setTabTitle(Messages.type.teams); // Drive data driveAPP.disableSF = !privateData.enableSF && AppConfig.disableSharedFolders; // Toolbar var $bar = $('#cp-toolbar'); var configTb = { displayed: ['useradmin', 'pageTitle', 'newpad', 'limit', 'notifications'], pageTitle: Messages.type.teams, metadataMgr: metadataMgr, readOnly: privateData.readOnly, sfCommon: common, $container: $bar }; var toolbar = APP.toolbar = Toolbar.create(configTb); // Update the name in the user menu var $displayName = $bar.find('.' + Toolbar.constants.username); metadataMgr.onChange(function () { var name = getDisplayName(metadataMgr.getUserData().name); $displayName.text(name); }); $displayName.text(getDisplayName(user.name)); // Load the Team module var onEvent = function (obj) { var ev = obj.ev; var data = obj.data; if (ev === 'LEAVE_TEAM') { $('div.cp-team-cat-back').click(); return; } if (ev === 'ROSTER_CHANGE') { if (Number(APP.team) === Number(data)) { redrawRoster(common); } return; } if (ev === 'ROSTER_CHANGE_RIGHTS') { redrawTeam(common); return; } }; APP.module = common.makeUniversal('team', { onEvent: onEvent }); var hash = privateData.teamInviteHash; if (!hash && !driveAPP.loggedIn) { UI.alert(Messages.mustLogin, function () { common.setLoginRedirect('login'); }, {forefront: true}); return; } if (!hash) { delete mainCategories.link; } else if (!driveAPP.loggedIn) { delete mainCategories.list; delete mainCategories.create; } var $body = APP.$body = $('body').css('display', ''); loadMain(common); metadataMgr.onChange(function () { var $div = $('div.cp-team-list'); if ($div.length) { refreshList(common, function (content) { $div.empty().append(content); }); } var $divLink = $('div.cp-team-link').empty(); if ($divLink.length) { refreshLink(common, function (content) { $divLink.append(content); }); } var $divCreate = $('div.cp-team-create'); if ($divCreate.length) { refreshCreate(common, function (content) { $divCreate.empty().append(content); }); } }); var onDisconnect = function (teamId) { if (APP.team && teamId && APP.team !== teamId) { return; } setEditable(false); $body.addClass('cp-app-team-offline'); if (APP.team && driveAPP.refresh) { driveAPP.refresh(); } toolbar.failed(); UIElements.disconnectAlert(); }; var onReconnect = function (teamId) { if (APP.team && teamId && APP.team !== teamId) { return; } setEditable(true); $body.removeClass('cp-app-team-offline'); if (APP.team && driveAPP.refresh) { driveAPP.refresh(); } toolbar.reconnecting(); UIElements.reconnectAlert(); }; sframeChan.on('EV_DRIVE_LOG', function (msg) { UI.log(msg); }); sframeChan.on('EV_NETWORK_DISCONNECT', function (teamId) { onDisconnect(teamId); if (teamId && APP.teams[teamId]) { APP.teams[teamId].offline = true; } }); sframeChan.on('EV_NETWORK_RECONNECT', function (teamId) { onReconnect(teamId); if (teamId && APP.teams[teamId]) { APP.teams[teamId].offline = false; } }); common.onLogout(function () { setEditable(false); }); }); }; main(); });