Merge branch 'soon'

pull/1/head
yflory 5 years ago
commit 5762246ec9

@ -4,7 +4,7 @@ define([
'/bower_components/chainpad-crypto/crypto.js',
'/common/common-util.js',
'/common/outer/network-config.js',
'/customize/credential.js',
'/common/common-credential.js',
'/bower_components/chainpad/chainpad.dist.js',
'/common/common-realtime.js',
'/common/common-constants.js',

@ -103,7 +103,7 @@ define([
])*/
])
]),
h('div.cp-version-footer', "CryptPad v3.2.0 (Chilihueque)")
h('div.cp-version-footer', "CryptPad v3.3.0 (Dodo)")
]);
};

@ -97,7 +97,7 @@ define([
]);*/
var _link = h('a', {
href: "https://opencollective.com/cryptpad/contribute",
href: "https://opencollective.com/cryptpad/",
target: '_blank',
rel: 'noopener',
});

@ -29,6 +29,7 @@
// Logs to show that something has happened
// These show only once
.alertify-logs {
z-index: 100001; // alertify logs should be in front of alertify modals
@media print {
@ -466,75 +467,10 @@
}
}
.cp-share-column {
.cp-share-grid, .cp-share-list {
.avatar_main(40px);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.cp-share-list {
margin-bottom: 15px;
}
.cp-share-grid {
.cp-usergrid-grid {
max-height: 225px;
overflow-x: auto;
}
.cp-recent-only {
.cp-share-grid, .cp-share-grid-filter {
display: none;
}
}
.cp-share-grid-filter {
display: flex;
input {
flex: 1;
min-width: 0;
margin-bottom: 0 !important;
&::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: @colortheme_alertify-primary-text;
opacity: 1; /* Firefox */
}
}
margin-bottom: 15px;
&:empty {
margin: 0;
display: none;
}
}
.cp-share-friend {
width: 70px;
height: 70px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding: 5px;
margin-bottom: 6px;
cursor: default;
transition: order 0.5s, background-color 0.5s;
margin-top: 1px;
.tools_unselectable();
&.cp-selected {
background-color: @colortheme_alertify-primary;
color: @colortheme_alertify-primary-text;
order: -1 !important;
}
.cp-share-friend-avatar {
min-height: 40px;
}
.cp-share-friend-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
text-align: center;
}
border: 1px solid @colortheme_alertify-primary;
&.cp-fake-friend {
visibility: hidden;
}
}
}
}

@ -96,9 +96,9 @@
@colortheme_drive-color: #fff;
@colortheme_drive-warn: #cd2532;
@colortheme_team-bg: #0b0061;
@colortheme_team-color: #fff;
@colortheme_team-warn: #cd2532;
@colortheme_teams-bg: #0b0061;
@colortheme_teams-color: #fff;
@colortheme_teams-warn: #cd2532;
@colortheme_file-bg: #cd2532;
@colortheme_file-color: #fff;

@ -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;
}
}

@ -242,7 +242,7 @@
margin-left: -2px;
}
.cp-app-drive-tree-docs {
margin-top: 20px;
margin-top: 15px;
//padding: 0 0 0 20px;
padding: 0;
cursor: auto;
@ -269,6 +269,12 @@
cursor: pointer;
margin-left: -5px;
padding-left: 5px;
.fa, .cptools {
display: inline-block;
min-width: 0;
width: 25px;
margin-right: 0px;
}
}
}
}

@ -121,6 +121,9 @@
margin: 5px 0px;
height: 1px;
background: #bbb;
& + hr {
display: none;
}
}
p {

@ -14,6 +14,7 @@
@import (reference) "./app-noscroll.less";
@import (reference) "./messenger.less";
@import (reference) "./cursor.less";
@import (reference) "./usergrid.less";
.framework_main(@bg-color, @warn-color, @color) {
--LessLoader_require: LessLoader_currentFile();
@ -40,6 +41,7 @@
.password_main();
.messenger_main();
.cursor_main();
.usergrid_main();
.creation_main(
@bg-color: @bg-color,
@color: @color
@ -73,6 +75,7 @@
.tippy_main();
.checkmark_main(20px);
.password_main();
.usergrid_main();
font: @colortheme_app-font;
}

@ -21,6 +21,7 @@
.cp-icon-color-sheet { color: @colortheme_oocell-bg; }
.cp-icon-color-kanban { color: @colortheme_kanban-bg; }
.cp-icon-color-admin { color: @colortheme_admin-bg; }
.cp-icon-color-teams { color: @colortheme_teams-bg; }
.cp-border-color-pad { border-color: @colortheme_pad-bg !important; }
.cp-border-color-code { border-color: @colortheme_code-bg !important; }
@ -39,5 +40,6 @@
.cp-border-color-sheet { border-color: @colortheme_oocell-bg !important; }
.cp-border-color-kanban { border-color: @colortheme_kanban-bg !important; }
.cp-border-color-admin { border-color: @colortheme_admin-bg !important; }
.cp-border-color-teams { border-color: @colortheme_teams-bg !important; }
}

@ -6,13 +6,16 @@
}
.leftside-menu-category_main() {
.unselectable_make();
padding: 5px 20px;
padding: 5px 15px;
margin: 15px 0;
cursor: pointer;
height: @variables_bar-height;
line-height: @variables_bar-height - 10px;
.fa, .cptools {
width: 25px;
display: inline-flex;
justify-content: center;
margin-right: 5px;
min-width: 25px;
}
&:hover {
background: rgba(0,0,0,0.05);

@ -53,12 +53,22 @@
font-weight: bold;
}
}
.cp-limit-upgrade {
padding: 0;
line-height: 25px;
height: 25px;
margin: 0 3px;
border-radius: 0;
.cp-limit-buttons {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
justify-content: space-evenly;
a {
height: 25px;
display: inline-flex;
align-items: center;
min-width: 200px;
width: 50%;
padding-top: 0;
padding-bottom: 0;
justify-content: center;
flex: 1;
}
}
}
}

@ -31,9 +31,42 @@
.cp-sidebarlayout-categories {
flex: 1;
.cp-sidebarlayout-category {
display: flex;
align-items: center;
.leftside-menu-category_main();
}
}
&.cp-leftside-narrow {
transition: width 0.2s;
width: 55px;
.cp-sidebarlayout-category {
display: flex;
max-width: 100%;
align-items: center;
.fa, .cptools {
margin-right: 0;
flex: 0;
}
span.cp-sidebarlayout-category-name {
//margin-left: 5px;
padding-left: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
display: none;
}
}
&:hover {
transition: width 0.5s;
width: 250px;
.cp-sidebarlayout-category {
span.cp-sidebarlayout-category-name {
display: inline;
}
}
}
}
}
#cp-sidebarlayout-rightside {
flex: 1;

@ -0,0 +1,91 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./avatar.less";
@import (reference) "./tools.less";
.usergrid_main() {
--LessLoader_require: LessLoader_currentFile();
};
& {
.cp-usergrid-container {
.cp-usergrid-grid {
display: flex;
flex-wrap: wrap;
margin-bottom: 6px;
}
&.cp-usergrid-empty {
.cp-usergrid-grid, .cp-usergrid-filter {
display: none;
}
}
.cp-usergrid-filter {
display: flex;
input {
flex: 1;
min-width: 0;
margin-bottom: 0 !important;
&::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: @colortheme_alertify-primary-text;
opacity: 1; /* Firefox */
}
}
margin-bottom: 15px;
&:empty {
margin: 0;
display: none;
}
}
.cp-usergrid-user {
width: 70px;
height: 70px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding: 5px;
margin-bottom: 6px;
margin-right: 6px;
cursor: default;
transition: order 0.5s, background-color 0.5s;
margin-top: 1px;
.tools_unselectable();
&:nth-child(6n) {
margin-right: 0;
}
&.cp-selected {
background-color: @colortheme_alertify-primary;
color: @colortheme_alertify-primary-text;
order: -1 !important;
}
.cp-usergrid-user-avatar {
min-height: 40px;
}
.cp-usergrid-user-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
text-align: center;
line-height: 18px;
}
border: 1px solid @colortheme_alertify-primary;
&:not(.large) {
.avatar_main(40px);
}
&.large {
.avatar_main(25px);
width: 140px;
height: 35px;
flex-flow: row;
margin-right: 15px;
margin-bottom: 1px;
&:nth-child(3n) {
margin-right: 0;
}
}
}
}
}

@ -116,7 +116,7 @@ server {
try_files $uri =404;
}
location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet)$ {
location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams)$ {
rewrite ^(.*)$ $1/ redirect;
}

@ -3,7 +3,7 @@
;(function () { 'use strict';
const nThen = require('nthen');
const Nacl = require('tweetnacl');
const Nacl = require('tweetnacl/nacl-fast');
const Crypto = require('crypto');
const Once = require("./lib/once");
const Meta = require("./lib/metadata");

@ -4,7 +4,7 @@ var nThen = require("nthen");
var Util = require("../../www/common/common-util");
var Nacl = require("tweetnacl");
var Nacl = require("tweetnacl/nacl-fast");
var Client = module.exports;

@ -15,6 +15,10 @@ var deduplicate = require("./deduplicate");
var commands = {};
var isValidOwner = function (owner) {
return typeof(owner) === 'string' && owner.length === 44;
};
// ["ADD_OWNERS", ["7eEqelGso3EBr5jHlei6av4r9w2B9XZiGGwA1EgZ-5I="], 1561623438989]
commands.ADD_OWNERS = function (meta, args) {
// bail out if args isn't an array
@ -30,6 +34,7 @@ commands.ADD_OWNERS = function (meta, args) {
var changed = false;
args.forEach(function (owner) {
if (!isValidOwner(owner)) { return; }
if (meta.owners.indexOf(owner) >= 0) { return; }
meta.owners.push(owner);
changed = true;
@ -90,6 +95,7 @@ commands.ADD_PENDING_OWNERS = function (meta, args) {
}
// or fill it
args.forEach(function (owner) {
if (!isValidOwner(owner)) { return; }
if (meta.pending_owners.indexOf(owner) >= 0) { return; }
meta.pending_owners.push(owner);
changed = true;
@ -134,7 +140,7 @@ commands.RESET_OWNERS = function (meta, args) {
}
// overwrite the existing owners with the new one
meta.owners = deduplicate(args);
meta.owners = deduplicate(args.filter(isValidOwner));
return true;
};

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "3.2.0",
"version": "3.3.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",

@ -1,7 +1,7 @@
/*@flow*/
/*jshint esversion: 6 */
/* Use Nacl for checking signatures of messages */
var Nacl = require("tweetnacl");
var Nacl = require("tweetnacl/nacl-fast");
/* globals Buffer*/
/* globals process */

@ -2,7 +2,7 @@
const Fs = require('fs');
const nThen = require('nthen');
const Pinned = require('./pinned');
const Nacl = require('tweetnacl');
const Nacl = require('tweetnacl/nacl-fast');
const Path = require('path');
const Pins = require('../lib/pins');
const Config = require('../lib/load-config');

@ -25,6 +25,12 @@ var store;
var pins;
var Log;
var blobs;
var startTime = +new Date();
var msSinceStart = function () {
return (+new Date()) - startTime;
};
nThen(function (w) {
// load the store which will be used for iterating over channels
// and performing operations like archival and deletion
@ -57,6 +63,8 @@ nThen(function (w) {
}
blobs = _;
}));
}).nThen(function () {
Log.info("EVICT_TIME_TO_LOAD_PINS", msSinceStart());
}).nThen(function (w) {
// this block will iterate over archived channels and removes them
// if they've been in cold storage for longer than your configured archive time
@ -276,6 +284,8 @@ nThen(function (w) {
};
store.listChannels(handler, w(done));
}).nThen(function () {
Log.info("EVICT_TIME_TO_RUN_SCRIPT", msSinceStart());
}).nThen(function () {
// the store will keep this script running if you don't shut it down
store.shutdown();

@ -1,6 +1,6 @@
/* jshint esversion: 6, node: true */
const Nacl = require('tweetnacl');
const Nacl = require('tweetnacl/nacl-fast');
const keyPair = Nacl.box.keyPair();
console.log("You've just generated a new key pair for your support mailbox.");

@ -3,7 +3,7 @@
var Client = require("../../lib/client/");
var Crypto = require("../../www/bower_components/chainpad-crypto");
var Mailbox = Crypto.Mailbox;
var Nacl = require("tweetnacl");
var Nacl = require("tweetnacl/nacl-fast");
var nThen = require("nthen");
var Pinpad = require("../../www/common/pinpad");
var Rpc = require("../../www/common/rpc");
@ -388,6 +388,108 @@ nThen(function (w) {
roster.add(data, w(function (err) {
if (err) { return void console.error(err); }
}));
}).nThen(function (w) {
var data = {};
data[alice.curveKeys.curvePublic] = {
role: "OWNER",
};
alice.roster.describe(data, w(function (err) {
if (!err) {
console.log("Members should not be able to add themselves as owners!");
process.exit(1);
}
console.log("Alice failed to promote herself to owner, as expected");
}));
}).nThen(function (w) {
var data = {};
data[alice.curveKeys.curvePublic] = {
role: "ADMIN",
};
alice.roster.describe(data, w(function (err) {
if (!err) {
console.log("Members should not be able to add themselves as admins!");
process.exit(1);
}
console.log("Alice failed to promote herself to admin, as expected");
}));
}).nThen(function (w) {
var data = {};
data[alice.curveKeys.curvePublic] = {
test: true,
};
alice.roster.describe(data, w(function (err) {
if (err) {
console.log("Unexpected error while describing an arbitrary attribute");
process.exit(1);
}
}));
}).nThen(function (w) {
var state = alice.roster.getState();
var alice_state = state.members[alice.curveKeys.curvePublic];
//console.log(alice_state);
if (typeof(alice_state.test) !== 'boolean') {
console.error("Arbitrary boolean attribute was not set");
process.exit(1);
}
var data = {};
data[alice.curveKeys.curvePublic] = {
test: null,
};
alice.roster.describe(data, w(function (err) {
if (err) {
console.error(err);
console.error("Expected removal of arbitrary attribute to be successfully applied");
console.log(alice.roster.getState());
process.exit(1);
}
}));
}).nThen(function (w) {
var data = {};
data[alice.curveKeys.curvePublic] = {
notifications: null,
};
alice.roster.describe(data, w(function (err) {
if (!err) {
console.error("Expected deletion of notifications channel to fail");
process.exit(1);
}
if (err !== 'INVALID_NOTIFICATIONS') {
console.log("UNEXPECTED ERROR 1231241245");
console.error(err);
process.exit(1);
}
console.log("Deletion of notifications channel failed as expected");
}));
}).nThen(function (w) {
var data = {};
data[alice.curveKeys.curvePublic] = {
displayName: null,
};
alice.roster.describe(data, w(function (err) {
if (!err) {
console.error("Expected deletion of displayName to fail");
process.exit(1);
}
if (err !== 'INVALID_DISPLAYNAME') {
console.log("UNEXPECTED ERROR 12352623465");
console.error(err);
process.exit(1);
}
console.log("Deletion of displayName failed as expected");
}));
}).nThen(function (w) {
alice.roster.checkpoint(w(function (err) {
if (!err) {
console.error("Members should not be able to send checkpoints!");
process.exit(0);
}
console.error("checkpoint by member failed as expected");
}));
}).nThen(function (w) {
console.log("STATE =", JSON.stringify(oscar.roster.getState(), null, 2));
@ -399,10 +501,6 @@ nThen(function (w) {
if (err) { return void console.log(err); }
console.log("STATE =", JSON.stringify(oscar.roster.getState(), null, 2));
}));
}).nThen(function () {
}).nThen(function (w) {
// oscar sends a checkpoint
oscar.roster.checkpoint(w(function (err) {
@ -427,6 +525,50 @@ nThen(function (w) {
}
console.log("Promoted Alice to ADMIN");
}));
}).nThen(function (w) {
var data = {};
data[bob.curveKeys.curvePublic] = {
notifications: Hash.createChannelId(),
displayName: "BORB",
};
alice.roster.add(data, w(function (err) {
if (err === 'ALREADY_PRESENT' || err === 'NO_CHANGE') {
return void console.log("Duplicate add command failed as expected");
}
if (err) {
console.error("Unexpected error", err);
process.exit(1);
}
if (!err) {
console.log("Duplicate add succeeded unexpectedly");
process.exit(1);
}
}));
}).nThen(function (w) {
alice.roster.checkpoint(w(function (err) {
if (!err) { return; }
console.error("Checkpoint by an admin failed unexpectedly");
console.error(err);
process.exit(1);
}));
}).nThen(function (w) {
oscar.roster.checkpoint(w(function (err) {
if (!err) { return; }
console.error("Checkpoint by an owner failed unexpectedly");
console.error(err);
process.exit(1);
}));
}).nThen(function (w) {
alice.roster.remove([
oscar.curveKeys.curvePublic,
], w(function (err) {
if (!err) {
console.error("Removal of owner by admin succeeded unexpectedly");
process.exit(1);
}
console.log("Removal of owner by admin failed as expected");
}));
}).nThen(function (w) {
// bob finally connects, this time with the lastKnownHash provided by oscar
var rosterKeys = Crypto.Team.deriveMemberKeys(sharedConfig.rosterSeed, bob.curveKeys);
@ -455,16 +597,123 @@ nThen(function (w) {
roster.stop();
});
}));
}).nThen(function (w) {
var bogus = {};
var curveKeys = makeCurveKeys();
bogus[curveKeys.curvePublic] = {
displayName: "chewbacca",
notifications: Hash.createChannelId(),
};
bob.roster.add(bogus, w(function (err) {
if (!err) {
console.error("Expected 'add' by member to fail");
process.exit(1);
}
console.log("'add' by member failed as expected");
}));
}).nThen(function (w) {
bob.roster.remove([
alice.curveKeys.curvePublic,
], w(function (err) {
if (!err) {
console.error("Removal of admin by member succeeded unexpectedly");
process.exit(1);
}
console.log("Removal of admin by member failed as expected");
}));
}).nThen(function (w) {
bob.roster.remove([
oscar.curveKeys.curvePublic,
alice.curveKeys.curvePublic
//alice.curveKeys.curvePublic
], w(function (err) {
if (err) { return void console.log("command failed as expected"); }
w.abort();
console.log("Expected command to fail!");
process.exit(1);
}));
}).nThen(function (w) {
var data = {};
data[bob.curveKeys.curvePublic] = {
displayName: 'BORB',
};
bob.roster.describe(data, w(function (err) {
if (err) {
console.error("self-description by a member failed unexpectedly");
process.exit(1);
}
}));
}).nThen(function (w) {
var data = {};
data[oscar.curveKeys.curvePublic] = {
displayName: 'NULL',
};
bob.roster.describe(data, w(function (err) {
if (!err) {
console.error("description of an owner by a member succeeded unexpectedly");
process.exit(1);
}
console.log("description of an owner by a member failed as expected");
}));
}).nThen(function (w) {
var data = {};
data[alice.curveKeys.curvePublic] = {
displayName: 'NULL',
};
bob.roster.describe(data, w(function (err) {
if (!err) {
console.error("description of an admin by a member succeeded unexpectedly");
process.exit(1);
}
console.log("description of an admin by a member failed as expected");
}));
}).nThen(function (w) {
var data = {};
data[bob.curveKeys.curvePublic] = {
displayName: "NULL",
};
alice.roster.describe(data, w(function (err) {
if (err) {
console.error("Description of member by admin failed unexpectedly");
console.error(err);
process.exit(1);
}
}));
}).nThen(function (w) {
alice.roster.metadata({
name: "BEST TEAM",
topic: "Champions de monde!",
cheese: "Camembert",
}, w(function (err) {
if (err) {
console.error("Metadata change by admin failed unexpectedly");
console.error(err);
process.exit(1);
}
}));
}).nThen(function (w) {
bob.roster.metadata({
name: "WORST TEAM",
topic: "not a good team",
}, w(function (err) {
if (!err) {
console.error("Metadata change by member should have failed");
process.exit(1);
}
}));
}).nThen(function (w) {
oscar.roster.metadata({
cheese: null, // delete a field that you don't want presenet
}, w(function (err) {
if (err) {
console.error(err);
process.exit(1);
}
}));
}).nThen(function (w) {
alice.roster.remove([bob.curveKeys.curvePublic], w(function (err) {
if (err) {

@ -1,7 +1,7 @@
var Fs = require("fs");
var Fse = require("fs-extra");
var Path = require("path");
var nacl = require("tweetnacl");
var nacl = require("tweetnacl/nacl-fast");
var nThen = require("nthen");
var Tasks = module.exports;

@ -92,8 +92,10 @@
* {
max-width:100%;
}
iframe[type="application/pdf"] {
max-height:50vh;
iframe[src$=".pdf"] {
width: 100%;
height: 80vh;
max-height: 90vh;
}
}
.markdown_main();

@ -11,7 +11,7 @@ define(function() {
* redirected to the drive.
* You should never remove the drive from this list.
*/
config.availablePadTypes = ['drive', 'team', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard',
config.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard',
/*'oodoc', 'ooslide',*/ 'file', 'todo', 'contacts'];
/* The registered only types are apps restricted to registered users.
* You should never remove apps from this list unless you know what you're doing. The apps
@ -20,7 +20,7 @@ define(function() {
* users and these users will be redirected to the login page if they still try to access
* the app
*/
config.registeredOnlyTypes = ['team', 'file', 'contacts', 'oodoc', 'ooslide', 'sheet', 'notifications'];
config.registeredOnlyTypes = ['teams', 'file', 'contacts', 'oodoc', 'ooslide', 'sheet', 'notifications'];
/* CryptPad is available is multiple languages, but only English and French are maintained
* by the developers. The other languages may be outdated, and any missing string for a langauge
@ -106,6 +106,7 @@ define(function() {
ooslide: 'fa-file-powerpoint-o',
sheet: 'fa-file-excel-o',
drive: 'fa-hdd-o',
teams: 'fa-users',
};
// Ability to create owned pads and expiring pads through a new pad creation screen.
@ -155,5 +156,7 @@ define(function() {
// by default by the CryptPad developers.
config.disableSharedFolders = false;
config.surveyURL = "https://survey.cryptpad.fr/index.php/672782";
return config;
});

@ -16,6 +16,7 @@ define(function () {
tokenKey: 'loginToken',
displayPadCreationScreen: 'displayPadCreationScreen',
deprecatedKey: 'deprecated',
MAX_TEAMS_SLOTS: 3,
// Sub
plan: 'CryptPad_plan',
// Apps

@ -1,9 +1,6 @@
define([
'/customize/application_config.js',
'/bower_components/scrypt-async/scrypt-async.min.js',
], function (AppConfig) {
(function () {
var factory = function (AppConfig, Scrypt) {
var Cred = {};
var Scrypt = window.scrypt;
Cred.MINIMUM_PASSWORD_LENGTH = typeof(AppConfig.minimumPasswordLength) === 'number'?
AppConfig.minimumPasswordLength: 8;
@ -86,4 +83,19 @@ define([
};
return Cred;
});
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
{}, //require("../../customize.dist/application_config.js"),
require("../bower_components/scrypt-async/scrypt-async.min.js")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/customize/application_config.js',
'/bower_components/scrypt-async/scrypt-async.min.js',
], function (AppConfig) {
return factory(AppConfig, window.scrypt);
});
}
}());

@ -530,7 +530,7 @@ Version 1
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(require("./common-util"), require("chainpad-crypto"), require("tweetnacl"));
module.exports = factory(require("./common-util"), require("chainpad-crypto"), require("tweetnacl/nacl-fast"));
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.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;

File diff suppressed because it is too large Load Diff

@ -13,7 +13,7 @@ define([
};
Notifier.notify = function (data) {
if (Visible.isSupported() && !Visible.currently()) {
if (Visible.isSupported() && (!Visible.currently() || (data && data.force))) {
if (data) {
var title = data.title;
if (document.title) { title += ' (' + document.title + ')'; }

File diff suppressed because it is too large Load Diff

@ -5,6 +5,10 @@
window.atob = window.atob || function (str) { return Buffer.from(str, 'base64').toString('binary'); }; // jshint ignore:line
window.btoa = window.btoa || function (str) { return new Buffer(str, 'binary').toString('base64'); }; // jshint ignore:line
Util.slice = function (A, start, end) {
return Array.prototype.slice.call(A, start, end);
};
Util.bake = function (f, args) {
if (typeof(args) === 'undefined') { args = []; }
if (!Array.isArray(args)) { args = [args]; }
@ -265,7 +269,7 @@
var to;
var g = function () {
window.clearTimeout(to);
to = window.setTimeout(f, ms);
to = window.setTimeout(Util.bake(f, Util.slice(arguments)), ms);
};
return g;
};

@ -42,7 +42,8 @@ define([
var origin = encodeURIComponent(window.location.hostname);
var common = window.Cryptpad = {
Messages: Messages,
donateURL: 'https://accounts.cryptpad.fr/#/donate?on=' + origin,
//donateURL: 'https://accounts.cryptpad.fr/#/donate?on=' + origin,
donateURL: "https://opencollective.com/cryptpad/",
upgradeURL: 'https://accounts.cryptpad.fr/#/?on=' + origin,
account: {},
};
@ -209,15 +210,23 @@ define([
// RPC
common.pinPads = function (pads, cb) {
postMessage("PIN_PADS", pads, function (obj) {
common.pinPads = function (pads, cb, teamId) {
var data = {
teamId: teamId,
pads: pads
};
postMessage("PIN_PADS", data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj.hash);
});
};
common.unpinPads = function (pads, cb) {
postMessage("UNPIN_PADS", pads, function (obj) {
common.unpinPads = function (pads, cb, teamId) {
var data = {
teamId: teamId,
pads: pads
};
postMessage("UNPIN_PADS", data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj.hash);
});
@ -830,8 +839,10 @@ define([
postMessage('GET_PAD_METADATA', data, cb);
};
// XXX Teams: change the password of a pad owned by the team
common.changePadPassword = function (Crypt, Crypto, href, newPassword, edPublic, cb) {
common.changePadPassword = function (Crypt, Crypto, data, cb) {
var href = data.href;
var newPassword = data.password;
var teamId = data.teamId;
if (!href) { return void cb({ error: 'EINVAL_HREF' }); }
var parsed = Hash.parsePadUrl(href);
if (!parsed.hash) { return void cb({ error: 'EINVAL_HREF' }); }
@ -842,6 +853,7 @@ define([
var oldSecret;
var oldMetadata;
var newSecret;
var privateData;
if (parsed.hashData.version >= 2) {
newSecret = Hash.getSecrets(parsed.type, parsed.hash, newPassword);
@ -874,16 +886,26 @@ define([
common.getPadMetadata({channel: oldChannel}, waitFor(function (metadata) {
oldMetadata = metadata;
}));
common.getMetadata(waitFor(function (err, data) {
if (err) {
waitFor.abort();
return void cb({ error: err });
}
privateData = data.priv;
}));
}).nThen(function (waitFor) {
// Get owners, mailbox and expiration time
var owners = oldMetadata.owners;
if (!Array.isArray(owners) || owners.indexOf(edPublic) === -1) {
optsPut.metadata.owners = owners;
// Check if we're allowed to change the password
var edPublic = teamId ? (privateData.teams[teamId] || {}).edPublic : privateData.edPublic;
var isOwner = Array.isArray(owners) && edPublic && owners.indexOf(edPublic) !== -1;
if (!isOwner) {
// We're not an owner, we shouldn't be able to change the password!
waitFor.abort();
return void cb({ error: 'EPERM' });
}
optsPut.metadata.owners = owners;
var mailbox = oldMetadata.mailbox;
if (mailbox) {
@ -933,15 +955,15 @@ define([
}).nThen(function (waitFor) {
common.removeOwnedChannel({
channel: oldChannel,
teamId: null // TODO
teamId: teamId
}, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
}));
common.unpinPads([oldChannel], waitFor());
common.pinPads([newSecret.channel], waitFor());
common.unpinPads([oldChannel], waitFor(), teamId);
common.pinPads([newSecret.channel], waitFor(), teamId);
}).nThen(function (waitFor) {
common.setPadAttribute('password', newPassword, waitFor(function (err) {
if (err) { warning = true; }
@ -995,7 +1017,7 @@ define([
var Cred, Block, Login;
Nthen(function (waitFor) {
require([
'/customize/credential.js',
'/common/common-credential.js',
'/common/outer/login-block.js',
'/customize/login.js'
], waitFor(function (_Cred, _Block, _Login) {

@ -892,6 +892,7 @@ define([
// Arrow keys to modify the selection
var onWindowKeydown = function (e) {
if (!$content.is(':visible')) { return; }
var $searchBar = $tree.find('#cp-app-drive-tree-search-input');
if (document.activeElement && document.activeElement.nodeName === 'INPUT') { return; }
if ($searchBar.is(':focus') && $searchBar.val()) { return; }
@ -1137,6 +1138,9 @@ define([
//hide.push('download');
hide.push('openincode');
}
if ($element.is('.cp-border-color-sheet')) {
hide.push('download');
}
if ($element.is('.cp-app-drive-element-file')) {
// No folder in files
hide.push('color');
@ -2263,7 +2267,7 @@ define([
var arr = [];
AppConfig.availablePadTypes.forEach(function (type) {
if (type === 'drive') { return; }
if (type === 'team') { return; }
if (type === 'teams') { return; }
if (type === 'contacts') { return; }
if (type === 'todo') { return; }
if (type === 'file') { return; }
@ -3245,6 +3249,11 @@ define([
if (!isVirtual && typeof(root) === "undefined") {
log(Messages.fm_unknownFolderError);
debug("Unable to locate the selected directory: ", path);
if (path.length === 1 && path[0] === ROOT) {
// Somehow we can't display ROOT. We should abort now because we'll
// end up in an infinite loop
return void UI.warn(Messages.fm_error_cantPin); // Internal server error, please reload...
}
var parentPath = path.slice();
parentPath.pop();
_displayDirectory(parentPath, true);
@ -3384,9 +3393,17 @@ define([
}
});*/
// If the selected element is not visible, scroll to make it visible, otherwise scroll to
// the previous scroll position
var $sel = findSelectedElements();
if ($sel.length) {
$sel[0].scrollIntoView();
var _top = $sel[0].getBoundingClientRect().top;
var _topContent = $content[0].getBoundingClientRect().top;
if ((_topContent + s + $content.height() - 20) < _top) {
$sel[0].scrollIntoView();
} else {
$content.scrollTop(s);
}
} else {
$content.scrollTop(s);
}
@ -3721,6 +3738,10 @@ define([
data.roHref = base + data.roHref;
}
if (currentPath[0] === TEMPLATE) {
data.isTemplate = true;
}
if (manager.isSharedFolder(el)) {
delete data.roHref;
//data.noPassword = true;

@ -113,8 +113,12 @@ define([
var notifyToolbar = function () {
if (!toolbar || !toolbar['chat']) { return; }
if (toolbar['chat'].find('button').hasClass('cp-toolbar-button-active')) { return; }
toolbar['chat'].find('button').addClass('cp-toolbar-notification');
if (!toolbar['chat'].find('button').hasClass('cp-toolbar-button-active')) {
toolbar['chat'].find('button').addClass('cp-toolbar-notification');
}
if (!toolbar['chat'].hasClass('cp-leftside-active')) {
toolbar['chat'].find('span.fa').addClass('cp-team-chat-notification');
}
};
var notify = function (id) {
@ -568,12 +572,15 @@ define([
var el_message = markup.message(message);
common.notify();
if (message.type === 'MSG') {
var name = typeof message.name !== "undefined" ?
(message.name || Messages.anonymous) :
contactsData[message.author].displayName;
common.notify({title: name, msg: message.text});
common.notify({
title: name,
msg: message.text,
force: toolbar && toolbar.team && !toolbar['chat'].hasClass('cp-leftside-active')
});
}
notifyToolbar();
@ -716,7 +723,7 @@ define([
if (room.isFriendChat) {
$parentEl = $userlist.find('.cp-app-contacts-friends');
} else if (room.isTeamChat) {
$parentEl = $userlist.find('.cp-app-contacts-padchat'); // XXX
$parentEl = $userlist.find('.cp-app-contacts-padchat');
} else if (room.isPadChat) {
$parentEl = $userlist.find('.cp-app-contacts-padchat');
} else {
@ -829,7 +836,7 @@ define([
return void console.error('Invalid team chat');
}
var room = rooms[0];
room.name = 'TEAMS'; // XXX
room.name = Messages.type.team;
rooms.forEach(initializeRoom);
});
};

@ -216,6 +216,9 @@ define([
// if not archived, add handlers
if (!content.archived) {
content.handler = function () {
if (msg.content.teamChannel) {
return void UIElements.displayAddTeamOwnerModal(common, data);
}
UIElements.displayAddOwnerModal(common, data);
};
}
@ -261,8 +264,7 @@ define([
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>";
// XXX
var text = Messages._getKey('team_invitedToTeam', [name, teamName]);
return text;
};
if (!content.archived) {
@ -280,8 +282,7 @@ define([
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var teamName = Util.fixHTML(Util.find(msg, ['content', 'teamName']) || '');
content.getFormatText = function () {
var text = name + " has kicked you from join the team <b>" + teamName +"</b>";
// XXX
var text = Messages._getKey('team_kickedFromTeam', [name, teamName]);
return text;
};
if (!content.archived) {
@ -296,10 +297,9 @@ define([
// 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');
var key = 'team_' + (msg.content.answer ? 'accept' : 'decline') + 'Invitation';
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>';
return Messages._getKey(key, [name, teamName]);
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);

@ -203,22 +203,36 @@ define([
/////////////////////// RPC //////////////////////////////////////
//////////////////////////////////////////////////////////////////
// pinPads needs to support the old format where data is an array of channel IDs
// and the new format where data is an object with "teamId" and "pads"
Store.pinPads = function (clientId, data, cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
if (!data) { return void cb({error: 'EINVAL'}); }
var s = getStore(data && data.teamId);
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
if (typeof(cb) !== 'function') {
console.error('expected a callback');
cb = function () {};
}
store.rpc.pin(data, function (e, hash) {
var pads = data.pads || data;
s.rpc.pin(pads, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
// unpinPads needs to support the old format where data is an array of channel IDs
// and the new format where data is an object with "teamId" and "pads"
Store.unpinPads = function (clientId, data, cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
if (!data) { return void cb({error: 'EINVAL'}); }
var s = getStore(data && data.teamId);
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
store.rpc.unpin(data, function (e, hash) {
var pads = data.pads || data;
s.rpc.unpin(pads, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
@ -503,6 +517,18 @@ define([
/////////////////////// Store ////////////////////////////////////
//////////////////////////////////////////////////////////////////
var getAllStores = function () {
var stores = [store];
var teamModule = store.modules['team'];
if (teamModule) {
var teams = teamModule.getTeams().map(function (id) {
return teamModule.getTeam(id);
});
Array.prototype.push.apply(stores, teams);
}
return stores;
};
// Get or create the user color for the cursor position
var getRandomColor = function () {
var getColor = function () {
@ -588,10 +614,14 @@ define([
s.manager.addPad(data.path, pad, function (e) {
if (e) { return void cb({error: e}); }
var send = data.teamId ? s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
}, clientId);
// Send a CHANGE events to all the teams because we may have just
// added a pad to a shared folder stored in multiple teams
getAllStores().forEach(function (_s) {
var send = _s.id ? _s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
}, clientId);
});
onSync(data.teamId, cb);
});
};
@ -606,19 +636,68 @@ define([
// No password for profile
list.push(Hash.hrefToHexChannelId('/profile/#' + store.proxy.profile.edit, null));
}
if (store.proxy.mailboxes) {
Object.keys(store.proxy.mailboxes || {}).forEach(function (id) {
if (id === 'supportadmin') { return; }
var m = store.proxy.mailboxes[id];
list.push(m.channel);
});
}
if (store.proxy.teams) {
Object.keys(store.proxy.teams || {}).forEach(function (id) {
var t = store.proxy.teams[id];
if (t.owner) {
list.push(t.channel);
list.push(t.keys.roster.channel);
list.push(t.keys.chat.channel);
}
});
}
return list;
};
var removeOwnedPads = function (waitFor) {
// Delete owned pads
var edPublic = Util.find(store, ['proxy', 'edPublic']);
var ownedPads = getOwnedPads();
var sem = Saferphore.create(10);
ownedPads.forEach(function (c) {
var w = waitFor();
sem.take(function (give) {
Store.removeOwnedChannel(null, c, give(function (obj) {
if (obj && obj.error) { console.error(obj.error); }
var otherOwners = false;
nThen(function (_w) {
Store.anonRpcMsg(null, {
msg: 'GET_METADATA',
data: c
}, _w(function (obj) {
if (obj && obj.error) {
give();
return void _w.abort();
}
var md = obj[0];
var isOwner = md && Array.isArray(md.owners) && md.owners.indexOf(edPublic) !== -1;
if (!isOwner) {
give();
return void _w.abort();
}
otherOwners = md.owners.some(function (ed) { return ed !== edPublic; });
}));
}).nThen(function (_w) {
if (otherOwners) {
Store.setPadMetadata(null, {
channel: c,
command: 'RM_OWNERS',
value: [edPublic],
}, _w());
return;
}
// We're the only owner: delete the pad
store.rpc.removeOwnedChannel(c, _w(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function () {
give();
w();
}));
});
});
});
};
@ -758,17 +837,6 @@ define([
* - attr (Array)
* - value (String)
*/
var getAllStores = function () {
var stores = [store];
var teamModule = store.modules['team'];
if (teamModule) {
var teams = teamModule.getTeams().map(function (id) {
return teamModule.getTeam(id);
});
Array.prototype.push.apply(stores, teams);
}
return stores;
};
Store.setPadAttribute = function (clientId, data, cb) {
nThen(function (waitFor) {
getAllStores().forEach(function (s) {
@ -931,7 +999,6 @@ define([
});
}).nThen(cb);
};
// XXX Teams. encrypted href...
Store.setPadTitle = function (clientId, data, cb) {
var title = data.title;
var href = data.href;
@ -1538,7 +1605,6 @@ define([
var href, title;
// XXX TEAMOWNER
if (!res.some(function (obj) {
if (obj.data &&
Array.isArray(obj.data.owners) && obj.data.owners.indexOf(edPublic) !== -1 &&
@ -1575,13 +1641,19 @@ define([
// Update owners and expire time in the drive
getAllStores().forEach(function (s) {
var allData = s.manager.findChannel(data.channel);
var changed = false;
allData.forEach(function (obj) {
if (Sortify(obj.data.owners) !== Sortify(metadata.owners)) {
changed = true;
}
obj.data.owners = metadata.owners;
obj.data.atime = +new Date();
if (metadata.expire) {
obj.data.expire = +metadata.expire;
}
});
// If we had to change the "owners" field, redraw the drive UI
if (!changed) { return; }
var send = s.sendEvent || sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
@ -1592,11 +1664,8 @@ define([
Store.setPadMetadata = function (clientId, data, cb) {
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
if (!data.command) { return void cb({ error: 'EINVAL' }); }
// XXX TEAMOWNER
// If owned by a team, we should use the team rpc here
// We'll need common-ui-elements to tell us the "owners" value or we can
// call getPadMetadata first
store.rpc.setMetadata(data, function (err, res) {
var s = getStore(data.teamId);
s.rpc.setMetadata(data, function (err, res) {
if (err) { return void cb({ error: err }); }
if (!Array.isArray(res) || !res.length) { return void cb({}); }
cb(res[0]);
@ -1743,10 +1812,14 @@ define([
//var data = cmdData.data;
var s = getStore(cmdData.teamId);
var cb2 = function (data2) {
var send = cmdData.teamId ? s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
}, clientId);
// Send the CHANGE event to all the stores because the command may have
// affected data from a shared folder used by multiple teams.
getAllStores().forEach(function (_s) {
var send = _s.id ? _s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
}, clientId);
});
onSync(cmdData.teamId, function () {
cb(data2);
});
@ -1922,7 +1995,8 @@ define([
broadcast([], "UPDATE_METADATA");
},
pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
}, waitFor, function (ev, data, clients, cb) {
}, waitFor, function (ev, data, clients, _cb) {
var cb = Util.once(_cb || function () {});
clients.forEach(function (cId) {
postMessage(cId, 'MAILBOX_EVENT', {
ev: ev,
@ -2009,20 +2083,20 @@ define([
loadUniversal(Team, 'team', waitFor);
cleanFriendRequests();
}).nThen(function () {
arePinsSynced(function (err, yes) {
if (!yes) {
resetPins(function (err) {
if (err) { return console.error(err); }
console.log('RESET DONE');
});
}
});
var requestLogin = function () {
broadcast([], "REQUEST_LOGIN");
};
if (store.loggedIn) {
arePinsSynced(function (err, yes) {
if (!yes) {
resetPins(function (err) {
if (err) { return console.error(err); }
console.log('RESET DONE');
});
}
});
/* This isn't truly secure, since anyone who can read the user's object can
set their local loginToken to match that in the object. However, it exposes
a UI that will work most of the time. */

@ -0,0 +1,95 @@
(function () {
var factory = function (Util, Cred, nThen) {
nThen = nThen; // XXX
var Invite = {};
/*
TODO key derivation
scrypt(seed, passwd) => {
curve: {
private,
public,
},
ed: {
private,
public,
}
cryptKey,
channel
}
*/
var BYTES_REQUIRED = 256;
Invite.deriveKeys = function (seed, passwd, cb) {
cb = cb; // XXX
// TODO validate has cb
// TODO onceAsync the cb
// TODO cb with err if !(seed && passwd)
Cred.deriveFromPassphrase(seed, passwd, BYTES_REQUIRED, function (bytes) {
var dispense = Cred.dispenser(bytes);
dispense = dispense; // XXX
// edPriv => edPub
// curvePriv => curvePub
// channel
// cryptKey
});
};
Invite.createSeed = function () {
// XXX
// return a seed
};
Invite.create = function (cb) {
cb = cb; // XXX
// TODO validate has cb
// TODO onceAsync the cb
// TODO cb with err if !(seed && passwd)
// required
// password
// validateKey
// creatorEdPublic
// for owner
// ephemeral
// signingKey
// for owner to write invitation
// derived
// edPriv
// edPublic
// for invitee ownership
// curvePriv
// curvePub
// for acceptance OR
// authenticated decline message via mailbox
// channel
// for owned deletion
// for team pinning
// cryptKey
// for protecting channel content
};
return Invite;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require("../common-util"),
require("../common-credential.js"),
require("nthen")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/common/common-credential.js',
'/bower_components/nthen/index.js',
], function (Util, Cred, nThen) {
return factory(Util, nThen);
});
}
}());

@ -270,12 +270,12 @@ define([
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (!content.href || !content.title || !content.channel) {
if (!content.teamChannel && !(content.href && content.title && content.channel)) {
console.log('Remove invalid notification');
return void cb(true);
}
var channel = content.channel;
var channel = content.channel || content.teamChannel;
if (addOwners[channel]) { return void cb(true); }
addOwners[channel] = {
@ -286,7 +286,7 @@ define([
cb(false);
};
removeHandlers['ADD_OWNER'] = function (ctx, box, data) {
var channel = data.content.channel;
var channel = data.content.channel || data.content.teamChannel;
if (addOwners[channel]) {
delete addOwners[channel];
}
@ -297,12 +297,23 @@ define([
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (!content.channel) {
if (!content.channel && !content.teamChannel) {
console.log('Remove invalid notification');
return void cb(true);
}
var channel = content.channel;
var channel = content.channel || content.teamChannel;
// If our ownership rights for a team have been removed, update the owner flag
if (content.teamChannel) {
var teams = ctx.store.proxy.teams || {};
Object.keys(teams).some(function (id) {
if (teams[id].channel === channel) {
teams[id].owner = false;
return true;
}
});
}
if (addOwners[channel] && content.pending) {
return void cb(false, addOwners[channel]);
@ -321,7 +332,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 +350,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 +372,10 @@ define([
return void cb(true);
}
if (invitedTo[content.teamChannel] && content.pending) {
return void cb(true, invitedTo[content.teamChannel]);
}
cb(false);
};

@ -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; }

@ -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);
@ -875,7 +899,6 @@ define([
var messenger = {};
var store = cfg.store;
if (AppConfig.availablePadTypes.indexOf('contacts') === -1) { return; }
if (!store.loggedIn || !store.proxy.edPublic) { return; }
var ctx = {
store: store,
updateMetadata: cfg.updateMetadata,
@ -883,7 +906,8 @@ define([
emit: emit,
friendsClients: [],
channels: {},
validateKeys: {}
validateKeys: {},
range_requests: {}
};
@ -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);
}

@ -171,6 +171,10 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
// if no role was provided, assume MEMBER
if (typeof(data.role) !== 'string') { data.role = 'MEMBER'; }
if (!canAddRole(author, data.role, members)) {
throw new Error("INSUFFICIENT_PERMISSIONS");
}
if (typeof(data.displayName) !== 'string') { throw new Error("DISPLAYNAME_REQUIRED"); }
if (typeof(data.notifications) !== 'string') { throw new Error("NOTIFICATIONS_REQUIRED"); }
});
@ -178,12 +182,9 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
var changed = false;
// then iterate again and apply it
Object.keys(args).forEach(function (curve) {
var data = args[curve];
if (!canAddRole(author, data.role, members)) { return; }
// this will result in a change
changed = true;
members[curve] = data;
members[curve] = args[curve];
});
return changed;
@ -241,11 +242,26 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
var current = Util.clone(members[curve]);
if (typeof(data.role) === 'string') { // they're trying to change the role...
// throw if they're trying to upgrade to something greater
if (!canAddRole(author, data.role, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
}
// DESCRIBE commands must initialize a displayName if it isn't already present
if (typeof(current.displayName) !== 'string' && typeof(data.displayName) !== 'string') { throw new Error('DISPLAYNAME_REQUIRED'); }
if (typeof(current.displayName) !== 'string' && typeof(data.displayName) !== 'string') {
throw new Error('DISPLAYNAME_REQUIRED');
}
if (['undefined', 'string'].indexOf(typeof(data.displayName)) === -1) {
throw new Error("INVALID_DISPLAYNAME");
}
// DESCRIBE commands must initialize a mailbox channel if it isn't already present
if (typeof(current.notifications) !== 'string' && typeof(data.displayName) !== 'string') { throw new Error('NOTIFICATIONS_REQUIRED'); }
if (typeof(current.notifications) !== 'string' && typeof(data.notifications) !== 'string') {
throw new Error('NOTIFICATIONS_REQUIRED');
}
if (['undefined', 'string'].indexOf(typeof(data.notifications)) === -1) {
throw new Error("INVALID_NOTIFICATIONS");
}
});
var changed = false;
@ -256,7 +272,9 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
var data = args[curve];
Object.keys(data).forEach(function (key) {
if (current[key] === data[key]) { return; }
// when null is passed as new data and it wasn't considered an invalid change
// remove it from the map. This is how you delete things properly
if (typeof(current[key]) !== 'undefined' && data[key] === null) { return void delete current[key]; }
current[key] = data[key];
});
@ -305,6 +323,12 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
return true;
};
var MANDATORY_METADATA_FIELDS = [
'avatar',
'name',
'topic',
];
// only admin/owner can change group metadata
commands.METADATA = function (args, author, roster) {
if (!isMap(args)) { throw new Error("INVALID_ARGS"); }
@ -313,6 +337,11 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
// validate inputs
Object.keys(args).forEach(function (k) {
if (args[k] === null) {
if (MANDATORY_METADATA_FIELDS.indexOf(k) === -1) { return; }
throw new Error('CANNOT_REMOVE_MANDATORY_METADATA');
}
// can't set metadata to anything other than strings
// use empty string to unset a value if you must
if (typeof(args[k]) !== 'string') { throw new Error("INVALID_ARGUMENTS"); }
@ -321,6 +350,11 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
var changed = false;
// {topic, name, avatar} are all strings...
Object.keys(args).forEach(function (k) {
if (typeof(roster.state.metadata[k]) !== 'undefined' && args[k] === null) {
changed = true;
delete roster.state.metadata[k];
}
// ignore things that won't cause changes
if (args[k] === roster.state.metadata[k]) { return; }
@ -446,7 +480,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
// deleted while you are open
// emit an event
var onChannelError = function (info) {
if (!ready) { return void cb(info); } // XXX make sure we don't reconnect
if (!ready) { return void cb(info); }
console.error("CHANNEL_ERROR", info);
};
@ -608,14 +642,19 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
Object.keys(data).forEach(function (curve) {
if (Object.keys(data).some(function (curve) {
var member = data[curve];
if (!isMap(member)) { delete data[curve]; }
// validate that you're trying to describe a user that is present
if (!isMap(state.members[curve])) { return true; }
// don't send fields that won't result in a change
Object.keys(member).forEach(function (k) {
if (member[k] === state.members[curve][k]) { delete member[k]; }
});
});
})) {
// returning true in the above loop indicates that something was invalid
return void cb("INVALID_ARGUMENTS");
}
send(['DESCRIBE', data], cb);
};

@ -16,14 +16,51 @@ define([
- config: network and "manager" (either the user one or a team manager)
- id: shared folder id
*/
var allSharedFolders = {};
SF.load = function (config, id, data, cb) {
var network = config.network;
var store = config.store;
var manager = store.manager;
var teamId = store.id || -1;
var handler = store.handleSharedFolder;
var parsed = Hash.parsePadUrl(data.href);
var secret = Hash.getSecrets('drive', parsed.hash, data.password);
var sf = allSharedFolders[secret.channel];
if (sf && sf.ready && sf.rt) {
// The shared folder is already loaded, return its data
setTimeout(function () {
var leave = function () { SF.leave(secret.channel, teamId); };
store.manager.addProxy(id, sf.rt.proxy, leave);
cb(sf.rt, sf.metadata);
});
sf.team.push(teamId);
if (handler) { handler(id, sf.rt); }
return sf.rt;
}
if (sf && sf.queue && sf.rt) {
// The shared folder is loading, add our callbacks to the queue
sf.queue.push({
cb: cb,
store: store,
id: id
});
sf.team.push(teamId);
if (handler) { handler(id, sf.rt); }
return sf.rt;
}
sf = allSharedFolders[secret.channel] = {
queue: [{
cb: cb,
store: store,
id: id
}],
team: [store.id || -1]
};
var owners = data.owners;
var listmapConfig = {
data: {},
@ -40,15 +77,42 @@ define([
owners: owners
}
};
var rt = Listmap.create(listmapConfig);
var rt = sf.rt = Listmap.create(listmapConfig);
rt.proxy.on('ready', function (info) {
manager.addProxy(id, rt.proxy, info.leave);
cb(rt, info.metadata);
if (!sf.queue) {
return;
}
sf.queue.forEach(function (obj) {
var leave = function () { SF.leave(secret.channel, teamId); };
obj.store.manager.addProxy(obj.id, rt.proxy, leave);
obj.cb(rt, info.metadata);
});
sf.leave = info.leave;
sf.metadata = info.metadata;
sf.ready = true;
delete sf.queue;
});
if (handler) { handler(id, rt); }
return rt;
};
SF.leave = function (channel, teamId) {
var sf = allSharedFolders[channel];
if (!sf) { return; }
var clients = sf.teams;
if (!Array.isArray(clients)) { return; }
var idx = clients.indexOf(teamId);
if (idx === -1) { return; }
// Remove the selected team
clients.splice(idx, 1);
//If all the teams have closed this shared folder, stop it
if (clients.length) { return; }
if (sf.rt && sf.rt.stop) {
sf.rt.stop();
}
};
/* loadSharedFolders
load all shared folder stored in a given drive
- store: user or team main store

@ -15,10 +15,11 @@ define([
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad/chainpad.dist.js',
'/bower_components/nthen/index.js',
'/bower_components/saferphore/index.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, Constants, Realtime,
ProxyManager, UserObject, SF, Roster, Messaging,
Listmap, Crypto, CpNetflux, ChainPad, nThen) {
Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) {
var Team = {};
var Nacl = window.nacl;
@ -37,7 +38,9 @@ define([
// Also pin the onlyoffice channels if they exist
if (n.rtChannel) { toPin.push(n.rtChannel); }
if (n.lastVersion) { toPin.push(n.lastVersion); }
team.pin(toPin, function (obj) { console.error(obj); });
team.pin(toPin, function (obj) {
if (obj && obj.error) { console.error(obj.error); }
});
}
// Unpin the deleted pads (deleted <=> changed to undefined)
if (p[0] === UserObject.FILES_DATA && typeof(o) === "object" && o.channel && !n) {
@ -50,7 +53,9 @@ define([
// Also unpin the onlyoffice channels if they exist
if (o.rtChannel) { toUnpin.push(o.rtChannel); }
if (o.lastVersion) { toUnpin.push(o.lastVersion); }
team.unpin(toUnpin, function (obj) { console.error(obj); });
team.unpin(toUnpin, function (obj) {
if (obj && obj.error) { console.error(obj); }
});
}
}
}
@ -73,8 +78,8 @@ define([
var closeTeam = function (ctx, teamId) {
var team = ctx.teams[teamId];
if (!team) { return; }
team.listmap.stop();
team.roster.stop();
try { team.listmap.stop(); } catch (e) {}
try { team.roster.stop(); } catch (e) {}
team.proxy = {};
delete ctx.teams[teamId];
delete ctx.store.proxy.teams[teamId];
@ -99,18 +104,6 @@ define([
if (membersChannel) { list.push(membersChannel); }
if (mailboxChannel) { list.push(mailboxChannel); }
// XXX Add the team mailbox
/*
if (store.proxy.mailboxes) {
var mList = Object.keys(store.proxy.mailboxes).map(function (m) {
return store.proxy.mailboxes[m].channel;
});
list = list.concat(mList);
}
*/
list.sort();
return list;
};
@ -185,7 +178,6 @@ define([
channel: secret.channel,
secret: secret,
validateKey: secret.keys.validateKey
// XXX owners: team owner + all admins?
};
};
@ -219,7 +211,9 @@ define([
SF.load({
network: ctx.store.network,
store: team
}, id, data, cb);
}, id, data, function (id, rt) {
cb(id, rt);
});
};
var manager = team.manager = ProxyManager.create(proxy.drive, {
onSync: function (cb) { ctx.Store.onSync(id, cb); },
@ -290,7 +284,9 @@ define([
};
var openChannel = function (ctx, teamData, id, cb) {
var openChannel = function (ctx, teamData, id, _cb) {
var cb = Util.once(_cb);
var secret = Hash.getSecrets('team', teamData.hash, teamData.password);
var crypto = Crypto.createEncryptor(secret.keys);
@ -298,7 +294,34 @@ define([
var roster;
var lm;
// Roster keys
var myKeys = {
curvePublic: ctx.store.proxy.curvePublic,
curvePrivate: ctx.store.proxy.curvePrivate
};
var rosterData = keys.roster || {};
var rosterKeys = rosterData.edit ? Crypto.Team.deriveMemberKeys(rosterData.edit, myKeys)
: Crypto.Team.deriveGuestKeys(rosterData.view || '');
nThen(function (waitFor) {
ctx.store.anon_rpc.send("IS_NEW_CHANNEL", secret.channel, waitFor(function (e, response) {
if (response && response.length && typeof(response[0]) === 'boolean' && response[0]) {
// Channel is empty: remove this team
delete ctx.store.proxy.teams[id];
waitFor.abort();
cb({error: 'ENOENT'});
}
}));
ctx.store.anon_rpc.send("IS_NEW_CHANNEL", rosterKeys.channel, waitFor(function (e, response) {
if (response && response.length && typeof(response[0]) === 'boolean' && response[0]) {
// Channel is empty: remove this team
delete ctx.store.proxy.teams[id];
waitFor.abort();
cb({error: 'ENOENT'});
}
}));
}).nThen(function (waitFor) {
// Load the proxy
var cfg = {
data: {},
@ -313,17 +336,25 @@ define([
userName: 'team',
classic: true
};
cfg.onMetadataUpdate = function () {
var team = ctx.teams[id];
if (!team) { return; }
ctx.emit('ROSTER_CHANGE', id, team.clients);
};
lm = Listmap.create(cfg);
lm.proxy.on('ready', waitFor());
lm.proxy.on('error', function (info) {
if (info && typeof (info.loaded) !== "undefined" && !info.loaded) {
cb({error:'ECONNECT'});
}
if (info && info.error) {
if (info.error === "EDELETED" ) {
closeTeam(ctx, id);
}
}
});
// Load the roster
var myKeys = {
curvePublic: ctx.store.proxy.curvePublic,
curvePrivate: ctx.store.proxy.curvePrivate
};
var rosterData = keys.roster || {};
var rosterKeys = rosterData.edit ? Crypto.Team.deriveMemberKeys(rosterData.edit, myKeys)
: Crypto.Team.deriveGuestKeys(rosterData.view || '');
Roster.create({
network: ctx.store.network,
channel: rosterKeys.channel,
@ -462,10 +493,15 @@ define([
}
}));
}).nThen(function () {
var id = Util.createRandomInteger();
config.onMetadataUpdate = function () {
var team = ctx.teams[id];
if (!team) { return; }
ctx.emit('ROSTER_CHANGE', id, team.clients);
};
var lm = Listmap.create(config);
var proxy = lm.proxy;
proxy.on('ready', function () {
var id = Util.createRandomInteger();
// Store keys in our drive
var keys = {
drive: {
@ -505,10 +541,119 @@ define([
if (info && typeof (info.loaded) !== "undefined" && !info.loaded) {
cb({error:'ECONNECT'});
}
if (info && info.error) {
if (info.error === "EDELETED") {
closeTeam(ctx, id);
}
}
});
});
};
var deleteTeam = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!team || !teamData) { return void cb ({error: 'ENOENT'}); }
var state = team.roster.getState();
var curvePublic = Util.find(ctx, ['store', 'proxy', 'curvePublic']);
var me = state.members[curvePublic];
if (!me || me.role !== "OWNER") { return cb({ error: "EFORBIDDEN"}); }
var edPublic = Util.find(ctx, ['store', 'proxy', 'edPublic']);
var teamEdPublic = Util.find(teamData, ['keys', 'drive', 'edPublic']);
nThen(function (waitFor) {
ctx.Store.anonRpcMsg(null, {
msg: 'GET_METADATA',
data: teamData.channel
}, waitFor(function (obj) {
// If we can't get owners, abort
if (obj && obj.error) {
waitFor.abort();
return cb({ error: obj.error});
}
// Check if we're an owner of the team drive
var metadata = obj[0];
if (metadata && Array.isArray(metadata.owners) &&
metadata.owners.indexOf(edPublic) !== -1) { return; }
// If w'e're not an owner, abort
waitFor.abort();
cb({error: 'EFORBIDDEN'});
}));
}).nThen(function (waitFor) {
team.proxy.delete = true;
// For each pad, check on the server if there are other owners.
// If yes, then remove yourself as an owner
// If no, delete the pad
var ownedPads = team.manager.getChannelsList('owned');
var sem = Saferphore.create(10);
ownedPads.forEach(function (c) {
var w = waitFor();
sem.take(function (give) {
var otherOwners = false;
nThen(function (_w) {
ctx.Store.anonRpcMsg(null, {
msg: 'GET_METADATA',
data: c
}, _w(function (obj) {
if (obj && obj.error) {
give();
return void _w.abort();
}
var md = obj[0];
var isOwner = md && Array.isArray(md.owners) && md.owners.indexOf(teamEdPublic) !== -1;
if (!isOwner) {
give();
return void _w.abort();
}
otherOwners = md.owners.some(function (ed) { return ed !== teamEdPublic; });
}));
}).nThen(function (_w) {
if (otherOwners) {
ctx.Store.setPadMetadata(null, {
channel: c,
command: 'RM_OWNERS',
value: [teamEdPublic],
}, _w());
return;
}
// We're the only owner: delete the pad
team.rpc.removeOwnedChannel(c, _w(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function () {
give();
w();
});
});
});
}).nThen(function (waitFor) {
// Delete the pins log
team.rpc.removePins(waitFor(function (err) {
if (err) { console.error(err); }
}));
// Delete the roster
var rosterChan = Util.find(teamData, ['keys', 'roster', 'channel']);
ctx.store.rpc.removeOwnedChannel(rosterChan, waitFor(function (err) {
if (err) { console.error(err); }
}));
// Delete the chat
var chatChan = Util.find(teamData, ['keys', 'chat', 'channel']);
ctx.store.rpc.removeOwnedChannel(chatChan, waitFor(function (err) {
if (err) { console.error(err); }
}));
// Delete the team drive
ctx.store.rpc.removeOwnedChannel(teamData.channel, waitFor(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function () {
cb();
closeTeam(ctx, teamId);
});
};
var joinTeam = function (ctx, data, cId, cb) {
var team = data.team;
if (!team.hash || !team.channel || !team.password
@ -540,11 +685,53 @@ define([
var getTeamRoster = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
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 || {};
// Get pending owners
var md = team.listmap.metadata || {};
if (Array.isArray(md.pending_owners)) {
// Get the members associated to the pending_owners' edPublic and mark them as such
md.pending_owners.forEach(function (ed) {
var member;
Object.keys(members).some(function (curve) {
if (members[curve].edPublic === ed) {
member = members[curve];
return true;
}
});
if (!member && teamData.owner) {
var removeOwnership = function (chan) {
ctx.Store.setPadMetadata(null, {
channel: chan,
command: 'RM_PENDING_OWNERS',
value: [ed],
}, function () {});
};
removeOwnership(teamData.channel);
removeOwnership(Util.find(teamData, ['keys', 'roster', 'channel']));
removeOwnership(Util.find(teamData, ['keys', 'chat', 'channel']));
return;
}
member.pendingOwner = true;
});
}
// 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) {
@ -573,6 +760,161 @@ define([
});
};
var offerOwnership = function (ctx, data, cId, _cb) {
var cb = Util.once(_cb);
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
if (!data.curvePublic) { return void cb({error: 'MISSING_DATA'}); }
var state = team.roster.getState();
var user = state.members[data.curvePublic];
nThen(function (waitFor) {
// Offer ownership to a friend
var onError = function (res) {
var err = res && res.error;
if (err) {
console.error(err);
waitFor.abort();
return void cb({error:err});
}
};
var addPendingOwner = function (chan) {
ctx.Store.setPadMetadata(null, {
channel: chan,
command: 'ADD_PENDING_OWNERS',
value: [user.edPublic],
}, waitFor(onError));
};
// Team proxy
addPendingOwner(teamData.channel);
// Team roster
addPendingOwner(Util.find(teamData, ['keys', 'roster', 'channel']));
// Team chat
addPendingOwner(Util.find(teamData, ['keys', 'chat', 'channel']));
}).nThen(function (waitFor) {
var obj = {};
obj[user.curvePublic] = {
role: 'OWNER'
};
team.roster.describe(obj, waitFor(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function (waitFor) {
// Send mailbox to offer ownership
var myData = Messaging.createData(ctx.store.proxy, false);
ctx.store.mailbox.sendTo("ADD_OWNER", {
teamChannel: teamData.channel,
chatChannel: Util.find(teamData, ['keys', 'chat', 'channel']),
rosterChannel: Util.find(teamData, ['keys', 'roster', 'channel']),
title: teamData.metadata.name,
user: myData
}, {
channel: user.notifications,
curvePublic: user.curvePublic
}, waitFor());
}).nThen(function () {
cb();
});
};
var revokeOwnership = function (ctx, teamId, user, _cb) {
var cb = Util.once(_cb);
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
var md = team.listmap.metadata || {};
var isPendingOwner = (md.pending_owners || []).indexOf(user.edPublic) !== -1;
nThen(function (waitFor) {
var cmd = isPendingOwner ? 'RM_PENDING_OWNERS' : 'RM_OWNERS';
var onError = function (res) {
var err = res && res.error;
if (err) {
console.error(err);
waitFor.abort();
return void cb(err);
}
};
var removeOwnership = function (chan) {
ctx.Store.setPadMetadata(null, {
channel: chan,
command: cmd,
value: [user.edPublic],
}, waitFor(onError));
};
// Team proxy
removeOwnership(teamData.channel);
// Team roster
removeOwnership(Util.find(teamData, ['keys', 'roster', 'channel']));
// Team chat
removeOwnership(Util.find(teamData, ['keys', 'chat', 'channel']));
}).nThen(function (waitFor) {
var obj = {};
obj[user.curvePublic] = {
role: 'ADMIN',
pendingOwner: false
};
team.roster.describe(obj, waitFor(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function (waitFor) {
// Send mailbox to offer ownership
var myData = Messaging.createData(ctx.store.proxy, false);
ctx.store.mailbox.sendTo("RM_OWNER", {
teamChannel: teamData.channel,
title: teamData.metadata.name,
pending: isPendingOwner,
user: myData
}, {
channel: user.notifications,
curvePublic: user.curvePublic
}, waitFor());
}).nThen(function () {
cb();
});
};
// We've received an offer to be an owner of the team.
// If we accept, we need to set the "owner" flag in our team data
// If we decline, we need to change our role back to "ADMIN"
var answerOwnership = function (ctx, data, cId, cb) {
var myTeams = ctx.store.proxy.teams;
var teamId;
Object.keys(myTeams).forEach(function (id) {
if (myTeams[id].channel === data.teamChannel) {
teamId = id;
return true;
}
});
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var obj = {};
// Accept
if (data.answer) {
teamData.owner = true;
return;
}
// Decline
obj[ctx.store.proxy.curvePublic] = {
role: 'ADMIN',
};
team.roster.describe(obj, function (err) {
if (err) { return void cb({error: err}); }
cb();
});
};
var describeUser = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
@ -580,6 +922,19 @@ define([
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
if (!data.curvePublic || !data.data) { return void cb({error: 'MISSING_DATA'}); }
var state = team.roster.getState();
var user = state.members[data.curvePublic];
// It it is an ownership revocation, we have to set it in pad metadata first
if (user.role === "OWNER" && data.data.role !== "OWNER") {
revokeOwnership(ctx, teamId, user, function (err) {
if (!err) { return; }
console.error(err);
return void cb({error: err});
});
return;
}
var obj = {};
obj[data.curvePublic] = data.data;
team.roster.describe(obj, function (err) {
@ -635,13 +990,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 +1065,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 +1105,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 +1133,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);
};
@ -800,6 +1169,12 @@ define([
if (cmd === 'SET_TEAM_METADATA') {
return void setTeamMetadata(ctx, data, clientId, cb);
}
if (cmd === 'OFFER_OWNERSHIP') {
return void offerOwnership(ctx, data, clientId, cb);
}
if (cmd === 'ANSWER_OWNERSHIP') {
return void answerOwnership(ctx, data, clientId, cb);
}
if (cmd === 'DESCRIBE_USER') {
return void describeUser(ctx, data, clientId, cb);
}
@ -815,6 +1190,9 @@ define([
if (cmd === 'REMOVE_USER') {
return void removeUser(ctx, data, clientId, cb);
}
if (cmd === 'DELETE_TEAM') {
return void deleteTeam(ctx, data, clientId, cb);
}
if (cmd === 'CREATE_TEAM') {
return void createTeam(ctx, data, clientId, cb);
}

@ -404,7 +404,7 @@ var factory = function (Util, Nacl) {
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(require("./common-util"), require("tweetnacl"));
module.exports = factory(require("./common-util"), require("tweetnacl/nacl-fast"));
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',

@ -321,7 +321,6 @@ define([
password: password,
channel: secret.channel,
enableSF: localStorage.CryptPad_SF === "1", // TODO to remove when enabled by default
enableTeams: localStorage.CryptPad_teams === "1",
devMode: localStorage.CryptPad_dev === "1",
fromFileData: Cryptpad.fromFileData ? {
title: Cryptpad.fromFileData.title
@ -377,6 +376,14 @@ define([
});
});
sframeChan.on('Q_GET_PINNED_USAGE', function (data, cb) {
Cryptpad.getPinnedUsage({}, function (e, used) {
cb({
error: e,
quota: used
});
});
});
sframeChan.on('Q_GET_PIN_LIMIT_STATUS', function (data, cb) {
Cryptpad.isOverPinLimit(null, function (e, overLimit, limits) {
cb({
@ -928,8 +935,8 @@ define([
});
sframeChan.on('Q_PAD_PASSWORD_CHANGE', function (data, cb) {
var href = data.href || window.location.href;
Cryptpad.changePadPassword(Cryptget, Crypto, href, data.password, edPublic, cb);
data.href = data.href || window.location.href;
Cryptpad.changePadPassword(Cryptget, Crypto, data, cb);
});
sframeChan.on('Q_CHANGE_USER_PASSWORD', function (data, cb) {
@ -1217,7 +1224,6 @@ define([
}
if (data.owned && data.team && data.team.edPublic) {
rtConfig.metadata.owners = [data.team.edPublic];
// XXX Teams mailbox
} else if (data.owned) {
rtConfig.metadata.owners = [edPublic];
rtConfig.metadata.mailbox = {};

@ -922,7 +922,6 @@ MessengerUI, Messages) {
var pads_options = [];
Config.availablePadTypes.forEach(function (p) {
if (p === 'drive') { return; }
if (p === 'team') { return; }
if (!Common.isLoggedIn() && Config.registeredOnlyTypes &&
Config.registeredOnlyTypes.indexOf(p) !== -1) { return; }
pads_options.push({

@ -12,7 +12,8 @@
"media": "Multimèdia",
"todo": "Tasques",
"contacts": "Contactes",
"sheet": "Full (Beta)"
"sheet": "Full (Beta)",
"teams": "Equips"
},
"button_newpad": "Nou document",
"button_newcode": "Nova pàgina de codi",
@ -40,7 +41,7 @@
"error": "Error",
"saved": "Desat",
"synced": "S'ha desat tot",
"deleted": "Document esborrat del vostre CryptDrive",
"deleted": "Esborrat",
"deletedFromServer": "Document esborrat del servidor",
"mustLogin": "Cal que inicieu la sessió per accedir a aquesta pàgina",
"disabledApp": "Aquesta aplicació està dehabilitada. Per a més informació, contacteu l'administració d'aquest CryptPad.",
@ -481,5 +482,30 @@
"settings_importDone": "S'ha acabat la importació",
"settings_autostoreTitle": "Emmagatzematge de documents a CryptDrive",
"edit": "Edita",
"autostore_sf": "Carpeta"
"autostore_sf": "Carpeta",
"padNotPinnedVariable": "Aquest document caducarà després de {4} dies d'inactivitat, {0}inicieu una sessió{1} o {2}registreu-vos{3} per conservar-lo.",
"uploadFolderButton": "Carregar una carpeta",
"fm_morePads": "Més",
"fc_color": "Canvia el color",
"fc_openInCode": "Obrir a l'editor de codi",
"fc_expandAll": "Desplega-ho tot",
"fc_collapseAll": "Plega-ho tot",
"register_emailWarning0": "Sembla que heu introduït la vostra adreça electrònica com identificador.",
"register_emailWarning1": "Podeu continuar, però aquestes dades no són necessàries i no s'enviaran al nostre servidor.",
"register_emailWarning2": "No podreu restablir la vostra contrasenya utilitzant la vostra adreça electrònica, com en molts altres serveis.",
"register_emailWarning3": "Si això us ha quedat clar i voleu seguir utilitzant la vostra adreça electrònica com identificador, cliqueu D'acord.",
"settings_autostoreHint": "L'emmagatzematge <b>Automàtic</b> dels documents permet de desar tots els documents que visiteu dins el vostre CryptDrive, sense que hàgiu de fer res de la vostra part.<br>L'emmagatzematge <b>Manual (sense preguntar)</b> permet que no es desin els documents automàticament. Sempre hi haurà l'opció de desar-los, però restarà oculta.",
"settings_autostoreYes": "Automàtic",
"settings_autostoreNo": "Manual (sense preguntar)",
"settings_autostoreMaybe": "Manual (preguntar sempre)",
"settings_userFeedbackTitle": "Retroacció",
"settings_userFeedbackHint1": "CryptPad pot enviar retroaccions molt limitades al servidor, de forma que ens permeti millorar l'experiència dels usuaris. ",
"settings_userFeedbackHint2": "El contingut dels vostres documents i les claus de desxifrat no es compartiran mai amb el servidor.",
"settings_userFeedback": "Activar l'enviament de retroaccions",
"settings_deleteTitle": "Supressió del compte",
"settings_deleteHint": "La supressió del vostre compte és permanent. El vostre CryptDrive i la vostra llista de documents seran suprimits del servidor. La resta de documents serà suprimida després de 90 dies d'inactivitat si ningú els ha desat al seu CryptDrive.",
"settings_deleteButton": "Suprimir el vostre compte",
"settings_deleteModal": "Envieu la següent informació a qui administri el vostre CryptPad per tal que les vostres dades siguin esborrades del servidor.",
"settings_deleteConfirm": "Segur que voleu suprimir el vostre compte? Aquesta acció és irreversible.",
"settings_deleted": "El vostre compte ha estat suprimit. Premeu D'acord per tornar a la pàgina inicial."
}

@ -12,7 +12,8 @@
"media": "Medien",
"todo": "Aufgaben",
"contacts": "Kontakte",
"sheet": "Tabelle (Beta)"
"sheet": "Tabelle (Beta)",
"teams": "Teams"
},
"button_newpad": "Neues Rich-Text-Pad",
"button_newcode": "Neues Code-Pad",
@ -38,7 +39,7 @@
"error": "Fehler",
"saved": "Gespeichert",
"synced": "Alles gespeichert",
"deleted": "Pad wurde aus deinem CryptDrive gelöscht",
"deleted": "Gelöscht",
"deletedFromServer": "Pad wurde vom Server gelöscht",
"mustLogin": "Du musst angemeldet sein, um auf diese Seite zuzugreifen",
"disabledApp": "Diese Anwendung wurde deaktiviert. Kontaktiere den Administrator dieses CryptPads, um mehr Informationen zu erhalten.",
@ -1157,8 +1158,67 @@
"owner_addButton": "Zur Eigentümerschaft einladen",
"owner_addConfirm": "Mit-Eigentümer können den Inhalt bearbeiten und dich als Eigentümer entfernen. Bist du sicher?",
"owner_request": "{0} möchte dich zum Eigentümer von <b>{1}</b> machen",
"owner_request_accepted": "{0} hat deine Einladung angenommen, ein Eigentümer von <b>{1}</b> zu sein",
"owner_request_accepted": "{0} hat deine Einladung akzeptiert, ein Eigentümer von <b>{1}</b> zu sein",
"owner_request_declined": "{0} hat deine Einladung abgelehnt, ein Eigentümer von <b>{1}</b> zu sein",
"owner_removed": "{0} hat dich als Eigentümer von <b>{1}</b> entfernt",
"owner_removedPending": "{0} hat die Einladung zur Eigentümerschaft von <b>{1}</b> zurückgezogen"
"owner_removedPending": "{0} hat die Einladung zur Eigentümerschaft von <b>{1}</b> zurückgezogen",
"share_linkTeam": "Mit einem Team teilen",
"team_inviteModalButton": "Einladen",
"team_pickFriends": "Freunde auswählen, um sie in dieses Team einzuladen",
"team_noFriend": "Du bist derzeit mit keinen Freunden auf CryptPad verbunden.",
"team_pcsSelectLabel": "Speichern in",
"team_pcsSelectHelp": "Die Erstellung eines eigenen Pads im Drive deines Teams gibt die Eigentümerschaft an das Team.",
"team_invitedToTeam": "{0} hat dich zum Team eingeladen: <b>{1}</b>",
"team_kickedFromTeam": "{0} hat dich aus dem Team entfernt: <b>{1}</b>",
"team_acceptInvitation": "{0} hat deine Einladung zum Team akzeptiert: <b>{1}</b>",
"team_declineInvitation": "{0} hat deine Einladung zum Team abgelehnt: <b>{1}</b>",
"team_cat_general": "Über",
"team_cat_list": "Teams",
"team_cat_create": "Neu",
"team_cat_back": "Zurück zu Teams",
"team_cat_members": "Mitglieder",
"team_cat_chat": "Chat",
"team_cat_drive": "Drive",
"team_cat_admin": "Administration",
"team_infoLabel": "Über Teams",
"team_listLoad": "Öffnen",
"team_createLabel": "Ein neues Team erstellen",
"team_createName": "Teamname",
"team_rosterPromote": "Befördern",
"team_rosterDemote": "Degradieren",
"team_rosterKick": "Aus dem Team entfernen",
"team_inviteButton": "Freunde einladen",
"team_leaveButton": "Dieses Team verlassen",
"team_leaveConfirm": "Wenn du dieses Team verlässt, verlierst du den Zugriff auf das dazugehörige CryptDrive, den Chatverlauf und andere Inhalte. Bist du sicher?",
"team_owner": "Eigentümer",
"team_admins": "Admins",
"team_members": "Mitglieder",
"team_nameTitle": "Teamname",
"team_nameHint": "Name des Teams festlegen",
"team_avatarTitle": "Teamavatar",
"team_avatarHint": "Maximale Größe ist 500 KB (png, jpg, jpeg, gif)",
"team_infoContent": "Jedes Team hat eigene CryptDrives, Speicherplatzbegrenzungen, Chats und Mitgliederlisten. Eigentümer können das gesamte Team löschen. Admins können Mitglieder einladen oder entfernen. Mitglieder können das Team verlassen.",
"team_maxOwner": "Jeder Benutzer kann nur Eigentümer eines Teams sein.",
"team_maxTeams": "Jeder Benutzer kann nur Mitglied von {0} Teams sein.",
"team_listTitle": "Deine Teams",
"team_listSlot": "Verfügbare Teamplätze",
"owner_addTeamText": "... oder ein Team",
"owner_team_add": "{0} möchte dich zum Eigentümer des Teams <b>{1}</b> machen. Bist du damit einverstanden?",
"team_rosterPromoteOwner": "Eigentümerschaft anbieten",
"team_ownerConfirm": "Mit-Eigentümer können das Team ändern öder löschen, sowie dich als Eigentümer entfernen. Bist du sicher?",
"team_kickConfirm": "{0} wird über die Entfernung aus dem Team benachrichtigt. Bist du sicher?",
"sent": "Nachricht versandt",
"team_pending": "Eingeladen",
"team_deleteTitle": "Löschung des Teams",
"team_deleteHint": "Das Team und alle Dokumente, die ausschließlich Eigentum des Teams sind, löschen.",
"team_deleteButton": "Löschen",
"team_deleteConfirm": "Du bist gerade dabei, alle Daten eines Teams zu löschen. Andere Teammitglieder können dann möglicherweise nicht mehr auf ihre Daten zugreifen. Dies kann nicht rückgängig gemacht werden. Bist du sicher, dass du fortfahren möchtest?",
"team_pendingOwner": "(ausstehend)",
"team_pendingOwnerTitle": "Dieser Administrator hat dein Angebot auf Eigentümerschaft noch nicht akzeptiert.",
"team_demoteMeConfirm": "Du bist gerade dabei, deine Rechte aufzugeben. Dies kann nicht rückgängig gemacht werden. Bist du sicher?",
"crowdfunding_button2": "CryptPad unterstützen",
"survey": "CryptPad-Umfrage",
"team_title": "Team: {0}",
"team_quota": "Speicherplatzbegrenzung deines Teams",
"drive_quota": "Deine Speicherplatzbegrenzung"
}

@ -12,7 +12,8 @@
"media": "Média",
"todo": "Todo",
"contacts": "Contacts",
"sheet": "Tableur (Beta)"
"sheet": "Tableur (Beta)",
"teams": "Équipes"
},
"button_newpad": "Nouveau document texte",
"button_newcode": "Nouvelle page de code",
@ -39,7 +40,7 @@
"error": "Erreur",
"saved": "Enregistré",
"synced": "Tout est enregistré",
"deleted": "Pad supprimé de votre CryptDrive",
"deleted": "Supprimé",
"deletedFromServer": "Pad supprimé du serveur",
"mustLogin": "Vous devez être enregistré pour avoir accès à cette page",
"disabledApp": "Cette application a été désactivée. Pour plus d'information, veuillez contacter l'administrateur de ce CryptPad.",
@ -310,7 +311,7 @@
"fm_templateName": "Modèles",
"fm_searchName": "Recherche",
"fm_recentPadsName": "Pads récents",
"fm_ownedPadsName": "Pads en votre possession",
"fm_ownedPadsName": "Propriétaire",
"fm_tagsName": "Mots-clés",
"fm_sharedFolderName": "Dossier partagé",
"fm_searchPlaceholder": "Rechercher...",
@ -999,10 +1000,12 @@
"crowdfunding_home1": "CryptPad a besoin d'aide !",
"crowdfunding_home2": "Cliquez sur le bouton pour découvrir notre campagne de financement participatif.",
"crowdfunding_button": "Soutenir CryptPad",
"crowdfunding_button2": "Aider CryptPad",
"crowdfunding_popup_text": "<h3>Aider CryptPad</h3>Pour vous assurer que CryptPad soit activement développé, nous vous invitons à supporter le projet via la <a href=\"https://opencollective.com/cryptpad\">page OpenCollective</a>, où vous pouvez trouver notre <b>Roadmap</b> et nos <b>objectifs de financement</b>.",
"crowdfunding_popup_yes": "Voir la page",
"crowdfunding_popup_no": "Pas maintenant",
"crowdfunding_popup_never": "Ne plus demander",
"survey": "Enquête CryptPad",
"markdown_toc": "Sommaire",
"debug_getGraph": "Obtenir le code permettant de générer un graphe de ce document",
"debug_getGraphWait": "Génération du graphe... Veuillez patienter.",
@ -1160,5 +1163,62 @@
"owner_request_declined": "{0} a refusé votre offre de devenir propriétaire de <b>{1}</b>",
"owner_removed": "{0} a supprimé vos droits de propriétaire de <b>{1}</b>",
"owner_removedPending": "{0} a annulé l'offre de co-propriété reçue pour <b>{1}</b>",
"padNotPinnedVariable": "Ce pad va expirer après {4} jours d'inactivité, {0}connectez-vous{1} ou {2}enregistrez-vous{3} pour le préserver."
"padNotPinnedVariable": "Ce pad va expirer après {4} jours d'inactivité, {0}connectez-vous{1} ou {2}enregistrez-vous{3} pour le préserver.",
"share_linkTeam": "Partager avec une équipe",
"team_pickFriends": "Choisissez les amis à inviter dans cette équipe",
"team_inviteModalButton": "Inviter",
"team_noFriend": "Vous n'avez pas encore ajouté d'ami sur CryptPad.",
"team_pcsSelectLabel": "Sauver dans",
"team_pcsSelectHelp": "Créer un pad dans le drive d'une équipe rend cette équipe propriétaire du pad si l'option est cochée.",
"team_invitedToTeam": "{0} vous à inviter à rejoindre l'équipe : <b>{1}</b>",
"team_kickedFromTeam": "{0} vous a exclu de l'équipe : <b>{1}</b>",
"team_acceptInvitation": "{0} a accepté votre offre de rejoindre l'équipe : <b>{1}</b>",
"team_declineInvitation": "{0} a refusé votre offre de rejoindre l'équipe : <b>{1}</b>",
"team_cat_general": "À propos",
"team_cat_list": "Équipes",
"team_cat_create": "Nouvelle équipe",
"team_cat_back": "Retour aux équipes",
"team_cat_members": "Membres",
"team_cat_chat": "Chat",
"team_cat_drive": "Drive",
"team_cat_admin": "Administration",
"team_infoLabel": "À propos des équipes",
"team_listLoad": "Ouvrir",
"team_createLabel": "Créer une nouvelle équipe",
"team_createName": "Nom de l'équipe",
"team_rosterPromote": "Promouvoir",
"team_rosterDemote": "Rétrograder",
"team_rosterKick": "Expulser de l'équipe",
"team_inviteButton": "Inviter des amis",
"team_leaveButton": "Quitter cette équipe",
"team_leaveConfirm": "Si vous quittez cette équipe, vous perdrez l'accès à son CryptDrive, son chat et les autres contenus. Êtes-vous sûr ?",
"team_owner": "Propriétaires",
"team_admins": "Administrateurs",
"team_members": "Membres",
"team_nameTitle": "Nom de l'équipe",
"team_nameHint": "Définir le nom de l'équipe",
"team_avatarTitle": "Avatar de l'équipe",
"team_avatarHint": "500 Ko maximum (png, jpg, jpeg, gif)",
"team_infoContent": "Chaque équipe possède son propre CryptDrive, sa limite de stockage, son chat et sa liste de membres. Les Propriétaires de l'équipe peuvent la supprimer complètement, les Administrateurs peuvent inviter ou expulser des members et les Membres peuvent quitter l'équipe.",
"team_maxOwner": "Chaque compte utilisateur ne peut être propriétaire que d'une seule équipe.",
"team_maxTeams": "Chaque compte utilisateur ne peut être membre que de {0} équipes.",
"team_listTitle": "Vos équipes",
"team_listSlot": "Emplacement d'équipe disponible",
"owner_addTeamText": "...ou à une équipe",
"owner_team_add": "{0} souhaite que vous soyez propriétaire de l'équipe <b>{1}</b>. Acceptez-vous ?",
"team_rosterPromoteOwner": "Proposer d'être propriétaire",
"team_ownerConfirm": "Les co-propriétaires seront en mesure de modifier ou supprimer l'équipe et pourront supprimer vos droits de propriétaire. Continuer ?",
"team_kickConfirm": "{0} sera informé que vous l'avez expulsé de l'équipe. Êtes-vous sûr ?",
"sent": "Message envoyé",
"team_pending": "Invité",
"team_deleteTitle": "Suppression de l'équipe",
"team_deleteHint": "Supprimer l'équipe et tous les documents dont elle est exclusivement propriétaire.",
"team_deleteButton": "Supprimer",
"team_deleteConfirm": "Vous êtes sur le point de supprimer les données d'une équipe entière. Cette action peut impacter l'accès à leur données pour d'autres membres de l'équipe. La suppression est irréversible. Êtes-vous sûr de vouloir continuer ?",
"team_pendingOwner": "(en attente)",
"team_pendingOwnerTitle": "Cet administrateur n'a pas encore accepté l'offre de devenir propriétaire de l'équipe.",
"team_demoteMeConfirm": "Vous êtes sur le point de renoncer à vos droits. Vous ne serez pas en mesure d'annuler cette action. Continuer ?",
"team_title": "Équipe : {0}",
"team_quota": "Limite de stockage de votre équipe",
"drive_quota": "Votre limite de stockage"
}

@ -12,7 +12,8 @@
"media": "Media",
"todo": "Todo",
"contacts": "Contacts",
"sheet": "Sheet (Beta)"
"sheet": "Sheet (Beta)",
"teams": "Teams"
},
"button_newpad": "New Rich Text pad",
"button_newcode": "New Code pad",
@ -41,7 +42,7 @@
"error": "Error",
"saved": "Saved",
"synced": "Everything is saved",
"deleted": "Pad deleted from your CryptDrive",
"deleted": "Deleted",
"deletedFromServer": "Pad deleted from the server",
"mustLogin": "You must be logged in to access this page",
"disabledApp": "This application has been disabled. Contact the administrator of this CryptPad for more information.",
@ -1019,10 +1020,12 @@
"crowdfunding_home1": "CryptPad needs your help!",
"crowdfunding_home2": "Click on the button to learn about our crowdfunding campaign.",
"crowdfunding_button": "Support CryptPad",
"crowdfunding_button2": "Help CryptPad",
"crowdfunding_popup_text": "<h3>We need your help!</h3>To ensure that CryptPad is actively developed, consider supporting the project via the <a href=\"https://opencollective.com/cryptpad\">OpenCollective page</a>, where you can see our <b>Roadmap</b> and <b>Funding goals</b>.",
"crowdfunding_popup_yes": "Go to OpenCollective",
"crowdfunding_popup_no": "Not now",
"crowdfunding_popup_never": "Don't ask me again",
"survey": "CryptPad survey",
"markdown_toc": "Contents",
"fm_expirablePad": "This pad will expire on {0}",
"admin_authError": "Only administrators can access this page",
@ -1160,5 +1163,62 @@
"owner_request_accepted": "{0} has accepted your offer to be an owner of <b>{1}</b>",
"owner_request_declined": "{0} has declined your offer to be an owner of <b>{1}</b>",
"owner_removed": "{0} has removed your ownership of <b>{1}</b>",
"owner_removedPending": "{0} has canceled your ownership offer for <b>{1}</b>"
"owner_removedPending": "{0} has canceled your ownership offer for <b>{1}</b>",
"share_linkTeam": "Share with a team",
"team_pickFriends": "Choose which friends to invite to this team",
"team_inviteModalButton": "Invite",
"team_noFriend": "You haven't connected with any friends on CryptPad yet.",
"team_pcsSelectLabel": "Store in",
"team_pcsSelectHelp": "Creating an owned pad in your team's drive will give ownership to the team.",
"team_invitedToTeam": "{0} has invited you to join their team: <b>{1}</b>",
"team_kickedFromTeam": "{0} has kicked you from the team: <b>{1}</b>",
"team_acceptInvitation": "{0} has accepted your offer to join the team: <b>{1}</b>",
"team_declineInvitation": "{0} has declined your offer to join the team: <b>{1}</b>",
"team_cat_general": "About",
"team_cat_list": "Teams",
"team_cat_create": "New",
"team_cat_back": "Back to teams",
"team_cat_members": "Members",
"team_cat_chat": "Chat",
"team_cat_drive": "Drive",
"team_cat_admin": "Administration",
"team_infoLabel": "About teams",
"team_listLoad": "Open",
"team_createLabel": "Create a new team",
"team_createName": "Team name",
"team_rosterPromote": "Promote",
"team_rosterDemote": "Demote",
"team_rosterKick": "Kick from the team",
"team_inviteButton": "Invite friends",
"team_leaveButton": "Leave this team",
"team_leaveConfirm": "If you leave this team you will lose access to its CryptDrive, chat history, and other contents. Are you sure?",
"team_owner": "Owners",
"team_admins": "Admins",
"team_members": "Members",
"team_nameTitle": "Team name",
"team_nameHint": "Set the team's name",
"team_avatarTitle": "Team avatar",
"team_avatarHint": "500KB maximum size (png, jpg, jpeg, gif)",
"team_infoContent": "Each team has its own CryptDrive, storage quota, chat, and members list. Team owners can delete the whole team, Admins can invite or kick members, members can leave the team.",
"team_maxOwner": "Each user account is restricted to owning a single team.",
"team_maxTeams": "Each user account can only be a member of {0} teams.",
"team_listTitle": "Your teams",
"team_listSlot": "Available team slot",
"owner_addTeamText": "...or a team",
"owner_team_add": "{0} wants you to be an owner of the team <b>{1}</b>. Do you accept?",
"team_rosterPromoteOwner": "Offer ownership",
"team_ownerConfirm": "Co-owners can modify or delete the team and remove you as an owner. Are you sure?",
"team_kickConfirm": "{0} will know that you removed them from the team. Are you sure?",
"sent": "Message sent",
"team_pending": "Invited",
"team_deleteTitle": "Team deletion",
"team_deleteHint": "Delete the team and all documents owned exclusively by the team.",
"team_deleteButton": "Delete",
"team_deleteConfirm": "You are about to delete all of an entire team's data. This may impact other team members access to their data. This cannot be undone. Are you sure you want to proceed?",
"team_pendingOwner": "(pending)",
"team_pendingOwnerTitle": "This administrator has not accepted the ownership offer yet.",
"team_demoteMeConfirm": "You are about to give up your rights. You will not be able to undo this action. Are you sure?",
"team_title": "Team: {0}",
"team_quota": "Your team's storage limit",
"drive_quota": "Your storage limit"
}

@ -163,6 +163,7 @@ define([
common.createUsageBar(null, function (err, $limitContainer) {
if (err) { return void DriveUI.logError(err); }
APP.$limit = $limitContainer;
$limitContainer.attr('title', Messages.drive_quota);
}, true);
}

@ -821,7 +821,8 @@ define([
UI.errorLoadingScreen(errorText);
throw new Error(errorText);
}
} else {
}
if (!proxy.metadata || typeof(proxy.metadata.title) === "undefined") {
Title.updateTitle(Title.defaultTitle);
}

@ -3,7 +3,7 @@ define([
'/customize/login.js',
'/common/cryptpad-common.js',
'/common/test.js',
'/customize/credential.js', // preloaded for login.js
'/common/common-credential.js',
'/common/common-interface.js',
'/common/common-util.js',
'/common/common-realtime.js',

@ -9,7 +9,7 @@ define([
'/common/common-hash.js',
'/customize/messages.js',
'/common/hyperscript.js',
'/customize/credential.js',
'/common/common-credential.js',
'/customize/application_config.js',
'/api/config',
'/common/make-backup.js',

@ -3,6 +3,7 @@
@import (reference) '../../customize/src/less2/include/alertify.less';
@import (reference) '../../customize/src/less2/include/checkmark.less';
@import (reference) '../../customize/src/less2/include/password-input.less';
@import (reference) '../../customize/src/less2/include/usergrid.less';
&.cp-app-share {
.tippy_main();
@ -10,4 +11,5 @@
.checkmark_main(20px);
.password_main();
.modal_main();
.usergrid_main();
}

@ -94,7 +94,6 @@ define([
password: config.data.password,
isTemplate: config.data.isTemplate,
file: config.data.file,
enableTeams: localStorage.CryptPad_teams === "1",
};
for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }

@ -236,6 +236,10 @@ define([
APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container);
var sFrameChan = common.getSframeChannel();
sFrameChan.onReady(waitFor());
common.getPinUsage(null, waitFor(function (err, data) {
if (err) { return void console.error(err); }
APP.pinUsage = data;
}));
}).nThen(function (/*waitFor*/) {
createToolbar();
metadataMgr = common.getMetadataMgr();
@ -244,7 +248,7 @@ define([
APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly;
APP.support = Support.create(common, false);
APP.support = Support.create(common, false, APP.pinUsage);
// Content
var $rightside = APP.$rightside;

@ -4,8 +4,10 @@ define([
'/api/config',
'/common/dom-ready.js',
'/common/requireconfig.js',
'/common/sframe-common-outer.js'
], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) {
'/common/sframe-common-outer.js',
'/common/outer/local-store.js',
'/common/outer/login-block.js',
], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO, LocalStore, Block) {
var requireConfig = RequireConfig();
// Loaded in load #2
@ -43,6 +45,12 @@ define([
}
var addData = function (obj) {
if (category) { obj.category = category; }
var hash = LocalStore.getBlockHash();
if (!hash) { return; }
var parsed = Block.parseBlockHash(hash);
if (!parsed || !parsed.href) { return; }
obj.blockLocation = parsed.href;
};
SFCommonO.start({
noRealtime: true,

@ -24,7 +24,16 @@ define([
curvePublic: user.curvePublic,
edPublic: privateData.edPublic,
notifications: user.notifications,
blockLocation: privateData.blockLocation || '',
};
if (typeof(ctx.pinUsage) === 'object') {
// pass pin.usage, pin.limit, and pin.plan if supplied
Object.keys(ctx.pinUsage).forEach(function (k) {
data.sender[k] = ctx.pinUsage[k];
});
}
data.id = id;
data.time = +new Date();
@ -169,6 +178,8 @@ define([
]);
$(userData).click(function () {
$(userData).find('pre').toggle();
}).find('pre').click(function (ev) {
ev.stopPropagation();
});
var name = Util.fixHTML(content.sender.name) || Messages.anonymous;
@ -202,11 +213,12 @@ define([
]);
};
var create = function (common, isAdmin) {
var create = function (common, isAdmin, pinUsage) {
var ui = {};
var ctx = {
common: common,
isAdmin: isAdmin
isAdmin: isAdmin,
pinUsage: pinUsage || false,
};
ui.sendForm = function (id, form, dest) {

@ -1,87 +0,0 @@
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/drive.less';
@import (reference) '../../customize/src/less2/include/messenger.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
@import (reference) "../../customize/src/less2/include/tools.less";
&.cp-app-team {
.framework_min_main(
@bg-color: @colortheme_team-bg,
@warn-color: @colortheme_team-warn,
@color: @colortheme_team-color
);
.drive_main();
.messenger_main();
.sidebar-layout_main();
@roster-bg-color: #efefef;
#cp-sidebarlayout-container {
div#cp-sidebarlayout-rightside.cp-rightside-drive {
padding: 0;
& > .cp-team-chat {
display: flex;
height: 100%;
margin: 0;
.cp-app-contacts-container {
height: 100%;
}
}
& > .cp-team-drive {
display: flex;
height: 100%;
margin: 0;
.cp-app-drive-container {
height: 100%;
}
}
}
.cp-team-list-avatar {
.avatar_main(30px);
}
.cp-team-avatar {
.avatar_main(300px);
}
.cp-team-roster {
.avatar_main(50px);
.cp-team-roster-member {
display: flex;
align-items: center;
margin: 5px;
padding: 2px;
max-width: 500px;
background-color: @roster-bg-color;
&:hover {
background-color: darken(@roster-bg-color, 5%);
}
.cp-avatar {
margin-right: 10px;
}
.cp-team-member-name {
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.tools_unselectable();
}
.cp-team-member-actions {
.fa {
height: 25px;
width: 25px;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
background-color: darken(@roster-bg-color, 10%);
}
}
}
}
}
}
}

@ -0,0 +1,185 @@
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/drive.less';
@import (reference) '../../customize/src/less2/include/messenger.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
@import (reference) "../../customize/src/less2/include/tools.less";
&.cp-app-team {
.framework_min_main(
@bg-color: @colortheme_teams-bg,
@warn-color: @colortheme_teams-warn,
@color: @colortheme_teams-color
);
.drive_main();
.messenger_main();
.sidebar-layout_main();
@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-leftside {
background-color: #e0e0e0;
}
div#cp-sidebarlayout-rightside.cp-rightside-drive {
padding: 0;
& > .cp-team-chat {
display: flex;
height: 100%;
margin: 0;
.cp-app-contacts-container {
height: 100%;
}
}
& > .cp-team-drive {
display: flex;
height: 100%;
margin: 0;
.cp-app-drive-container {
height: 100%;
}
}
.cp-limit-buttons {
display: none;
}
}
.cp-leftside-narrow {
.cp-avatar {
overflow: visible;
media-tag {
margin-right: 0;
}
span.cp-sidebarlayout-category-name {
margin-left: 3px;
}
}
}
.cp-team-cat-header {
justify-content: center;
.avatar_main(30px);
.cp-avatar {
justify-content: center;
font-size: 20px;
}
media-tag {
order: -1;
margin-right: 3px;
}
cursor: default !important;
font-size: 18px;
&:hover {
background-color: transparent !important;
}
}
.cp-team-cat-chat {
span.cp-team-chat-notification {
color: red;
}
}
.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);
}
.cp-team-roster {
.avatar_main(50px);
.cp-team-roster-member {
display: flex;
align-items: center;
margin: 5px;
padding: 2px;
max-width: 500px;
background-color: @roster-bg-color;
&:hover {
background-color: darken(@roster-bg-color, 5%);
}
.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;
white-space: nowrap;
text-overflow: ellipsis;
.tools_unselectable();
}
.cp-team-member-actions {
.fa {
height: 25px;
width: 25px;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
background-color: darken(@roster-bg-color, 10%);
}
}
}
}
}
}
}

@ -2,7 +2,7 @@
<html class="cp-app-noscroll">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/team/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/teams/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
#editor1 { display: none; }

@ -3,9 +3,11 @@ define([
'/common/toolbar3.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',
@ -16,15 +18,17 @@ define([
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/team/app-team.less',
'less!/teams/app-team.less',
], function (
$,
Toolbar,
DriveUI,
Util,
Hash,
UI,
UIElements,
Feedback,
Constants,
nThen,
SFCommon,
ProxyManager,
@ -76,15 +80,15 @@ define([
var setEditable = DriveUI.setEditable;
var mainCategories = {
'general': [
'cp-team-info',
],
'list': [
'cp-team-list',
],
'create': [
'cp-team-create',
],
'general': [
'cp-team-info',
],
};
var teamCategories = {
'back': {
@ -94,10 +98,16 @@ define([
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.buildUI(common);
if (APP.usageBar) {
APP.usageBar.stop();
APP.usageBar = null;
}
});
});
}
@ -112,8 +122,10 @@ define([
'cp-team-chat'
],
'admin': [
'cp-team-edpublic',
'cp-team-name',
'cp-team-avatar'
'cp-team-avatar',
'cp-team-delete',
],
};
@ -138,6 +150,22 @@ define([
var categories = team ? teamCategories : mainCategories;
var active = team ? 'drive' : '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);
}
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; }
@ -164,8 +192,13 @@ define([
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');
@ -173,10 +206,17 @@ define([
showCategories(categories[key]);
});
$category.append(Messages['team_cat_'+key] || key); // XXX
$category.append(h('span.cp-sidebarlayout-category-name', Messages['team_cat_'+key] || key));
});
if (active === 'drive') {
APP.$rightside.addClass('cp-rightside-drive');
APP.$leftside.on('mouseover', function() {
APP.$leftside.addClass('cp-leftside-narrow');
APP.$leftside.off('mouseover');
});
} else {
APP.$rightside.removeClass('cp-rightside-drive');
APP.$leftside.removeClass('cp-leftside-narrow');
}
showCategories(categories[active]);
};
@ -217,6 +257,7 @@ define([
APP.usageBar = common.createUsageBar(APP.team, function (err, $limitContainer) {
if (err) { return void DriveUI.logError(err); }
driveAPP.$limit = $limitContainer;
$limitContainer.attr('title', Messages.team_quota);
}, true);
driveAPP.team = id;
var drive = DriveUI.create(common, {
@ -259,34 +300,57 @@ define([
makeBlock('info', function (common, cb) {
cb([
h('h3', 'Team application'), // XXX
h('p', 'From here you can ...') // XXX
h('h3', Messages.team_infoLabel),
h('p', Messages.team_infoContent)
]);
});
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', 'Open');
var avatar = h('span.cp-avatar.cp-team-list-avatar');
lis.push(h('li', h('ul', [
h('li', avatar), // XXX
h('li', 'Name: ' + team.metadata.name), // XXX
h('li', 'ID: ' + id), // XXX
h('li', a) // XXX
])));
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 () {
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); }
// Change title
$('.cp-toolbar-title-value').text(t);
sframeChan.event('EV_SET_TAB_TITLE', t);
// Load data
APP.team = id;
APP.teamEdPublic = Util.find(team, ['keys', 'drive', 'edPublic']);
buildUI(common, true, team.owner);
@ -294,7 +358,7 @@ define([
});
});
});
content.push(h('ul', lis));
content.push(h('div.cp-team-list-container', list));
cb(content);
});
return content;
@ -304,12 +368,30 @@ define([
});
makeBlock('create', function (common, cb) {
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var content = [];
content.push(h('h3', 'Create a team')); // XXX
content.push(h('label', 'Team name')); // XXX
var isOwner = Object.keys(privateData.teams || {}).some(function (id) {
return privateData.teams[id].owner;
}) && !privateData.devMode;
var getWarningBox = function () {
return h('div.alert.alert-warning', {
role:'alert'
}, isOwner ? Messages.team_maxOwner : Messages._getKey('team_maxTeams', [MAX_TEAMS_SLOTS]));
};
if (Object.keys(privateData.teams || {}).length >= 3 || 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', 'Create'); // XXX
var button = h('button.btn.btn-success', Messages.creation_create);
content.push(h('br'));
content.push(h('br'));
content.push(button);
@ -319,20 +401,23 @@ define([
var name = $(input).val();
if (!name.trim()) { return; }
state = true;
UI.confirm('Are you sure?', function (yes) {
if (!yes) {
state = false;
return;
APP.module.execCommand('CREATE_TEAM', {
name: name
}, function (obj) {
if (obj && obj.error) {
console.error(obj.error);
return void UI.warn(Messages.error);
}
APP.module.execCommand('CREATE_TEAM', {
name: name
}, function () {
var $div = $('div.cp-team-list').empty();
refreshList(common, function (content) {
state = false;
$div.append(content);
$('div.cp-team-cat-list').click();
});
// 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);
$('div.cp-team-cat-list').click();
});
});
});
@ -374,10 +459,10 @@ define([
};
var ROLES = ['MEMBER', 'ADMIN', 'OWNER'];
var describeUser = function (common, data, icon) {
var describeUser = function (common, curvePublic, data, icon) {
APP.module.execCommand('DESCRIBE_USER', {
teamId: APP.team,
curvePublic: data.curvePublic,
curvePublic: curvePublic,
data: data
}, function (obj) {
if (obj && obj.error) {
@ -387,60 +472,115 @@ define([
redrawRoster(common);
});
};
var makeMember = function (common, data, me) {
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;
});
// Avatar
var avatar = h('span.cp-avatar.cp-team-member-avatar');
common.displayAvatar($(avatar), data.avatar, data.displayName);
// Name
var name = h('span.cp-team-member-name', data.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-team-member-actions');
var $actions = $(actions);
var isMe = me && me.curvePublic === data.curvePublic;
var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1;
var theirRole = ROLES.indexOf(data.role) || 0;
// If they're an admin and I am an owner, I can promote them to owner
if (!isMe && myRole > theirRole && theirRole === 1 && !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 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: 'Promote' // XXX
title: Messages.team_rosterPromote
});
$(promote).click(function () {
data.role = 'ADMIN';
$(promote).hide();
describeUser(common, data, promote);
describeUser(common, data.curvePublic, {
role: 'ADMIN'
}, 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 (!isMe && myRole >= theirRole && theirRole > 0) {
if (myRole >= theirRole && theirRole > 0 && !data.pending) {
var demote = h('span.fa.fa-angle-double-down', {
title: 'Demote' // XXX
title: Messages.team_rosterDemote
});
$(demote).click(function () {
data.role = ROLES[theirRole - 1] || 'MEMBER';
$(demote).hide();
describeUser(common, data, demote);
var todo = function () {
var role = ROLES[theirRole - 1] || 'MEMBER';
$(demote).hide();
describeUser(common, data.curvePublic, {
role: role
}, promote);
};
if (isMe) {
return void UI.confirm(Messages.team_demoteMeConfirm, function (yes) {
if (!yes) { return; }
todo();
});
}
todo();
});
$actions.append(demote);
if (!(isMe && myRole === 2 && !otherOwners)) {
$actions.append(demote);
}
}
// 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) {
// Note: we can't remove owners, we have to demote them first
if (!isMe && myRole > 0 && myRole >= theirRole && theirRole !== 2) {
var remove = h('span.fa.fa-times', {
title: 'Remove' // XXX
title: Messages.team_rosterKick
});
$(remove).click(function () {
$(remove).hide();
APP.module.execCommand('REMOVE_USER', {
teamId: APP.team,
curvePublic: data.curvePublic,
}, function (obj) {
if (obj && obj.error) {
$(remove).show();
return void UI.alert(Messages.error);
}
redrawRoster(common);
UI.confirm(Messages._getKey('team_kickConfirm', [Util.fixHTML(data.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);
@ -450,7 +590,8 @@ define([
var content = [
avatar,
name,
actions
actions,
status,
];
var div = h('div.cp-team-roster-member', {
title: data.displayName
@ -471,9 +612,9 @@ define([
var me = roster[userData.curvePublic] || {};
var owner = Object.keys(roster).filter(function (k) {
if (roster[k].pending) { return; }
return roster[k].role === "OWNER";
return roster[k].role === "OWNER" || roster[k].pendingOwner;
}).map(function (k) {
return makeMember(common, roster[k], me);
return makeMember(common, roster[k], me, roster);
});
var admins = Object.keys(roster).filter(function (k) {
if (roster[k].pending) { return; }
@ -487,15 +628,20 @@ define([
}).map(function (k) {
return makeMember(common, roster[k], me);
});
// XXX LEAVE the team button
// XXX INVITE to the team button
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);
// 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 invite = h('button.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
@ -517,9 +663,9 @@ define([
}
if (me && (me.role === 'ADMIN' || me.role === 'MEMBER')) {
var leave = h('button.btn.btn-danger', 'LEAVE THE TEAM');
var leave = h('button.btn.btn-danger', Messages.team_leaveButton);
$(leave).click(function () {
UI.confirm("Your're going to leave this team and lose access to its entire drive. Are you sure?", function (yes) {
UI.confirm(Messages.team_leaveConfirm, function (yes) {
if (!yes) { return; }
APP.module.execCommand('LEAVE_TEAM', {
teamId: APP.team
@ -533,14 +679,18 @@ define([
$header.append(leave);
}
var noPending = pending.length ? '' : '.cp-hidden';
return [
header,
h('h3', 'OWNER'), // XXX
h('h3', Messages.team_owner),
h('div', owner),
h('h3', 'ADMINS'), // XXX
h('h3', Messages.team_admins),
h('div', admins),
h('h3', 'MEMBERS'), // XXX
h('div', members)
h('h3', Messages.team_members),
h('div', members),
h('h3'+noPending, Messages.team_pending),
h('div'+noPending, pending)
];
};
makeBlock('roster', function (common, cb) {
@ -557,14 +707,37 @@ define([
teamId: APP.team
}, function (obj) {
if (obj && obj.error) {
return void UI.alert(Messages.error); // XXX
return void UI.alert(Messages.error);
}
common.setTeamChat(obj.channel);
MessengerUI.create($(container), common, true);
MessengerUI.create($(container), common, {
chat: $('.cp-team-cat-chat'),
team: true
});
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.getUserHrefFromKeys(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) {
var $inputBlock = $('<div>', {'class': 'cp-sidebarlayout-input-block'});
var $input = $('<input>', {
@ -578,6 +751,7 @@ define([
var todo = function () {
var newName = $input.val();
if (!newName.trim()) { return; }
$spinner.show();
APP.module.execCommand('GET_TEAM_METADATA', {
teamId: APP.team
@ -668,6 +842,40 @@ define([
});
}, true);
makeBlock('delete', function (common, cb) {
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 main = function () {
var common;
var readOnly;
@ -691,16 +899,12 @@ define([
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
if (!privateData.enableTeams) {
return void UI.errorLoadingScreen(Messages.comingSoon);
}
readOnly = driveAPP.readOnly = metadataMgr.getPrivateData().readOnly;
driveAPP.loggedIn = common.isLoggedIn();
if (!driveAPP.loggedIn) { throw new Error('NOT_LOGGED_IN'); }
common.setTabTitle('TEAMS (ALPHA)'); // XXX
common.setTabTitle(Messages.type.teams);
// Drive data
if (privateData.newSharedFolder) {
@ -712,7 +916,7 @@ define([
var $bar = $('#cp-toolbar');
var configTb = {
displayed: ['useradmin', 'pageTitle', 'newpad', 'limit', 'notifications'],
pageTitle: 'TEAMS (ALPHA)', // XXX
pageTitle: Messages.type.teams,
metadataMgr: metadataMgr,
readOnly: privateData.readOnly,
sfCommon: common,

@ -20,7 +20,7 @@ define([
window.rc = requireConfig;
window.apiconf = ApiConfig;
document.getElementById('sbox-iframe').setAttribute('src',
ApiConfig.httpSafeOrigin + '/team/inner.html?' + requireConfig.urlArgs +
ApiConfig.httpSafeOrigin + '/teams/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
@ -36,31 +36,7 @@ define([
};
window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) {
var teamId; // XXX
var afterSecrets = function (Cryptpad, Utils, secret, cb) {
return void cb();
/*
var hash = window.location.hash.slice(1);
if (hash && Utils.LocalStore.isLoggedIn()) {
return; // XXX How to add a shared folder?
// Add a shared folder!
Cryptpad.addSharedFolder(teamId, secret, function (id) {
window.CryptPad_newSharedFolder = id;
cb();
});
return;
} else if (hash) {
var id = Utils.Util.createRandomInteger();
window.CryptPad_newSharedFolder = id;
var data = {
href: Utils.Hash.getRelativeHref(window.location.href),
password: secret.password
};
return void Cryptpad.loadSharedFolder(id, data, cb);
}
cb();
*/
};
var teamId;
var addRpc = function (sframeChan, Cryptpad) {
sframeChan.on('Q_SET_TEAM', function (data, cb) {
teamId = data;
@ -72,11 +48,6 @@ define([
data.teamId = teamId;
Cryptpad.userObjectCommand(data, cb);
});
// XXX no drive restore in teams? you could restore old keys...
/*sframeChan.on('Q_DRIVE_RESTORE', function (data, cb) {
data.teamId = teamId;
Cryptpad.restoreDrive(data, cb);
});*/
sframeChan.on('Q_DRIVE_GETOBJECT', function (data, cb) {
if (!teamId) { return void cb({error: 'EINVAL'}); }
if (data && data.sharedFolder) {
@ -109,7 +80,6 @@ define([
});
};
SFCommonO.start({
afterSecrets: afterSecrets,
noHash: true,
noRealtime: true,
//driveEvents: true,
Loading…
Cancel
Save