Merge branch 'master' of github.com:xwiki-labs/cryptpad

pull/1/head
ansuz 5 years ago
commit 5b22406dd7

@ -1,3 +1,82 @@
# Elasmotherium release notes
## Goals
This is a small release, focused on bug fixes and UI improvements, while we're finalizing bigger team-centric features planned for the next release.
## Update notes
This is a pretty basic release:
1. stop your server
2. pull the latest source code
3. restart your server
## Features
* Media elements (images, videos, pdf, etc.) will now display a placeholder while they're being downloaded and decrypted.
* Media elements deleted from the server by their owner will now display a "broken/missing" image.
* The "auto-close brackets" option in the Code and Slide applications can now be disabled from the user settings.
* "Add item" and "Add board" buttons in Kanban have been moved to improve usability with small screens.
* The "transfer ownership" feature for pads has been extended to shared folders. It is now possible to offer ownership of a shared folder to a friend.
* For administrators
* Better sorting of support tickets in the administration panel. Unanswered messages will be displayed first.
* Add team configuration options in `customize/application_config.js`
* `maxTeamsSlots` defines the maximum number of teams a user can join (default is 3). Teams may significantly increase the loading time of pages and we consider 3 to be a good balance between usability and performances.
* `maxOwnedTeams` defines the number of teams a user can own (default is 1). This number prevent users to create many teams only to increase their storage limit.
## Bug fixes
* The "pad creation modal" (Ctrl+E) is now working everywhere in the drive.
* We've fixed the share button for unregistered users (https://github.com/xwiki-labs/cryptpad/issues/457).
* We've fixed an issue with newly created kanban items replacing existing ones.
* Transfering/offering pad ownership from a team to yourself is now working properly.
# Dodo release (v3.3.0)
## Goals
We've continued to prioritize the development of team-centric features in CryptPad. This release was focused on stabilizing the code for Teams and making them available to the users.
## Update notes
This is a pretty basic release:
1. stop your server
2. pull the latest source code
3. install the latest serverside dependencies with `npm install`
4. install the latest clientside dependencies with `bower update`
5. restart your server
Note: we've updated our Nginx configuration to fix any missing trailing slash in the URL for the newest applications: https://github.com/xwiki-labs/cryptpad/commit/d4e5b98c140c28417e008379ec7af7cdc235792b
## Features
* You can now create _Teams_ in CryptPad. They're available from a new _Teams_ application and provide a full CryptDrive that can be shared between multiple users.
* Each team has a list of members. There are currently 3 different access level for team members:
* Members: can add, delete and edit pads from the team
* Admins: can also invite their CryptPad friends to the team, kick members and promote members as "Admin"
* Owners: can also promote admins as "Owner", change the team name or avatar and delete the team
* Each team has its own storage limit (50 MB by default, the same as user accounts).
* A chat is available to all the team members
* Pads created from the team's drive will be stored in this drive. If they are created as _owned_ pads, they will be ownedcc by the team.
* You can share pads or folders from your drive with one of your teams and you can store pads or folders from your team to your personal drive.
* Each user can be a member of up to 3 teams. A user can't create a new Team if they are already _Owner_ of another one.
* We've done some server improvements to save CPU usage.
* We've also improved to the messenger module to save CPU and memory in the client.
* The support panel (administrator side) now provides more debugging information about the users who ask for help
* A link to the new CryptPad survey (https://survey.cryptpad.fr/index.php/672782?lang=en) has been added to the user menu
* This link can be changed or removed using the "surveyURL" key in `/customize/application_config.js`. An empty value will remove the link from the menu.
## Bug fixes
* We've fixed an issue preventing users to remove owned empty channels from the server
* Adding and editing new items to the kanban boards will now update the correct item from the board
* We've fixed an issue with shared folders loaded by unregistered users
* The default title is now always set in newly created polls
* Desktop notifications will now be displayed only once per connection to the server and not once per CryptPad tab in the browser
* The button to download a spreadsheet from the drive has been removed. This feature is not available yet and the button was doing nothing.
# Chilihueque release (v3.2.0)
## Goals

@ -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.4.0 (Elasmotherium)")
]);
};

@ -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;
.cp-limit-buttons {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
justify-content: space-evenly;
a {
height: 25px;
margin: 0 3px;
border-radius: 0;
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,41 @@
.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 {
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;
}
}
}
}
}

@ -0,0 +1,14 @@
/*
* You can override the translation text using this file.
* The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js)
in a 'customize' directory (/customize/translations/messages.{LANG}.js).
* If you want to check all the existing translation keys, you can open the internal language file
but you should not change it directly (/common/translations/messages.{LANG}.js)
*/
define(['/common/translations/messages.ja.js'], function (Messages) {
// Replace the existing keys in your copied file here:
// Messages.button_newpad = "New Rich Text Document";
return Messages;
});

@ -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.4.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;

@ -18,5 +18,10 @@
display: flex;
flex-flow: column;
.cp-support-container {
display: flex;
flex-flow: column;
}
}

@ -170,10 +170,36 @@ define([
var supportKey = ApiConfig.supportMailbox;
create['support-list'] = function () {
if (!supportKey || !APP.privateKey) { return; }
var $div = makeBlock('support-list');
$div.addClass('cp-support-container');
var $container = makeBlock('support-list');
var $div = $(h('div.cp-support-container')).appendTo($container);
var hashesById = {};
var reorder = function () {
var order = Object.keys(hashesById);
order.sort(function (id1, id2) {
var t1 = hashesById[id1];
var t2 = hashesById[id2];
if (!Array.isArray(t1)) { return 1; }
if (!Array.isArray(t2)) { return -1; }
var lastMsg1 = t1[t1.length - 1];
var lastMsg2 = t2[t2.length - 1];
var time1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'time']);
var time2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'time']);
var authorEd1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'sender', 'edPublic']);
var authorEd2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'sender', 'edPublic']);
var admin1 = ApiConfig.adminKeys.indexOf(authorEd1) !== -1;
var admin2 = ApiConfig.adminKeys.indexOf(authorEd2) !== -1;
// If one is answered and not the other, put the unanswered first
if (admin1 && !admin2) { return 1; }
if (!admin1 && admin2) { return -1; }
// Otherwise, sort them by time
return time2 - time1;
});
order.forEach(function (id, i) {
$div.find('[data-id="'+id+'"]').css('order', i);
});
};
// Register to the "support" mailbox
common.mailbox.subscribe(['supportadmin'], {
onMessage: function (data) {
@ -219,11 +245,13 @@ define([
});
}
$ticket.append(APP.support.makeMessage(content, hash));
reorder();
}
});
return $div;
return $container;
};
var checkAdminKey = function (priv) {
if (!supportKey) { return; }
return Hash.checkBoxKeyPair(priv, supportKey);

@ -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.
@ -146,14 +147,24 @@ define(function() {
// Workers allow us to run the websockets connection and open the user drive in a separate thread.
// SharedWorkers allow us to load only one websocket and one user drive for all the browser tabs,
// making it much faster to open new tabs.
// Warning: This is an experimental feature. It will be enabled by default once we're sure it's stable.
config.disableWorkers = false;
// Shared folder are in a beta-test state. They are likely to disappear from a user's drive
// spontaneously, resulting in the deletion of the entire folder's content.
// We highly recommend to keep them disabled until they are stable enough to be enabled
// by default by the CryptPad developers.
config.disableSharedFolders = false;
config.surveyURL = "https://survey.cryptpad.fr/index.php/672782";
// Teams are always loaded during the initial loading screen (for the first tab only if
// SharedWorkers are available). Allowing users to be members of multiple teams can
// make them have a very slow loading time. To avoid impacting the user experience
// significantly, we're limiting the number of teams per user to 3 by default.
// You can change this value here.
//config.maxTeamsSlots = 3;
// Each team is considered as a registered user by the server. Users and teams are indistinguishable
// in the database so teams will offer the same storage limits as users by default.
// It means that each team created by a user can increase their storage limit by +100%.
// We're limiting the number of teams each user is able to own to 1 in order to make sure
// users don't use "fake" teams (1 member) just to increase their storage limit.
// You can change the value here.
// config.maxOwnedTeams = 1;
return config;
});

@ -1,4 +1,4 @@
define(function () {
define(['/customize/application_config.js'], function (AppConfig) {
return {
// localStorage
userHashKey: 'User_hash',
@ -16,6 +16,8 @@ define(function () {
tokenKey: 'loginToken',
displayPadCreationScreen: 'displayPadCreationScreen',
deprecatedKey: 'deprecated',
MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 3,
MAX_TEAMS_OWNED: AppConfig.maxOwnedTeams || 1,
// 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) {
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;
@ -3731,7 +3752,7 @@ define([
data.sharedFolder = true;
}
if (manager.isFile(el) && data.roHref) { // Only for pads!
if ((manager.isFile(el) && data.roHref) || manager.isSharedFolder(el)) { // Only for pads!
sframeChan.query('Q_GET_PAD_METADATA', {
channel: data.channel
}, function (err, val) {

@ -435,9 +435,14 @@
return mediaObject;
}
mediaObject.tag.innerHTML = '<img style="width: 100px; height: 100px;">';
// Download the encrypted blob
download(src, function (err, u8Encrypted) {
if (err) {
if (err === "XHR_ERROR 404") {
mediaObject.tag.innerHTML = '<img style="width: 100px; height: 100px;" src="/images/broken.png">';
}
return void emit('error', err);
}
// Decrypt the blob

@ -113,8 +113,12 @@ define([
var notifyToolbar = function () {
if (!toolbar || !toolbar['chat']) { return; }
if (toolbar['chat'].find('button').hasClass('cp-toolbar-button-active')) { return; }
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);

@ -30,10 +30,19 @@ define(['/api/config'], function (ApiConfig) {
icon = DEFAULT_ALT;
}
return new Notification(title,{
var n = new Notification(title,{
icon: icon,
body: msg,
});
n.onclick = function () {
if (!document) { return; }
try {
parent.focus();
window.focus(); //just in case, older browsers
this.close();
} catch (e) {}
};
return n;
};
Module.system = function (msg, title, icon) {

@ -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 () {
@ -527,7 +553,7 @@ define([
// Get the metadata for sframe-common-outer
Store.getMetadata = function (clientId, data, cb) {
var disableThumbnails = Util.find(store.proxy, ['settings', 'general', 'disableThumbnails']);
var teams = store.modules['team'] && store.modules['team'].getTeamsData();
var teams = (store.modules['team'] && store.modules['team'].getTeamsData()) || {};
var metadata = {
// "user" is shared with everybody via the userlist
user: {
@ -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 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); }
w();
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,8 +999,10 @@ define([
});
}).nThen(cb);
};
// XXX Teams. encrypted href...
Store.setPadTitle = function (clientId, data, cb) {
if (store.offline) {
return void cb({ error: 'OFFLINE' });
}
var title = data.title;
var href = data.href;
var channel = data.channel;
@ -1538,7 +1608,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 +1644,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 +1667,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]);
@ -1742,11 +1814,19 @@ define([
if (!cmdData || !cmdData.cmd) { return; }
//var data = cmdData.data;
var s = getStore(cmdData.teamId);
if (s.offline) {
broadcast([], 'NETWORK_DISCONNECT');
return void cb({ error: 'OFFLINE' });
}
var cb2 = function (data2) {
var send = cmdData.teamId ? s.sendEvent : sendDriveEvent;
// 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 +2002,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,6 +2090,11 @@ define([
loadUniversal(Team, 'team', waitFor);
cleanFriendRequests();
}).nThen(function () {
var requestLogin = function () {
broadcast([], "REQUEST_LOGIN");
};
if (store.loggedIn) {
arePinsSynced(function (err, yes) {
if (!yes) {
resetPins(function (err) {
@ -2018,11 +2104,6 @@ define([
}
});
var requestLogin = function () {
broadcast([], "REQUEST_LOGIN");
};
if (store.loggedIn) {
/* 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. */
@ -2047,6 +2128,7 @@ define([
proxy.settings.general.allowUserFeedback = true;
}
returned.feedback = proxy.settings.general.allowUserFeedback;
Feedback.init(returned.feedback);
if (typeof(cb) === 'function') { cb(returned); }
@ -2162,9 +2244,11 @@ define([
});
rt.proxy.on('disconnect', function () {
store.offline = true;
broadcast([], 'NETWORK_DISCONNECT');
});
rt.proxy.on('reconnect', function (info) {
store.offline = false;
broadcast([], 'NETWORK_RECONNECT', {myId: info.myId});
});

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

@ -9,16 +9,18 @@ define([
'/common/outer/sharedfolder.js',
'/common/outer/roster.js',
'/common/common-messaging.js',
'/common/common-feedback.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/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) {
ProxyManager, UserObject, SF, Roster, Messaging, Feedback,
Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) {
var Team = {};
var Nacl = window.nacl;
@ -37,7 +39,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 +54,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); }
});
}
}
}
@ -68,13 +74,19 @@ define([
path: p
});
});
proxy.on('disconnect', function () {
team.offline = true;
});
proxy.on('reconnect', function (info) {
team.offline = false;
});
};
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 +111,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 +185,6 @@ define([
channel: secret.channel,
secret: secret,
validateKey: secret.keys.validateKey
// XXX owners: team owner + all admins?
};
};
@ -219,7 +218,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 +291,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 +301,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 +343,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 +500,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: {
@ -498,6 +541,7 @@ define([
proxy.drive = {};
onReady(ctx, id, lm, roster, keys, cId, function () {
Feedback.send('TEAM_CREATION');
ctx.updateMetadata();
cb();
});
@ -505,10 +549,120 @@ 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 () {
Feedback.send('TEAM_DELETION');
closeTeam(ctx, teamId);
cb();
});
};
var joinTeam = function (ctx, data, cId, cb) {
var team = data.team;
if (!team.hash || !team.channel || !team.password
@ -540,11 +694,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 +769,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 +931,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 +999,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 +1074,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 +1114,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 +1142,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 +1178,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 +1199,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);
}

@ -82,9 +82,11 @@ define([
// All occurences are returned, in drive or shared folders
var findChannel = function (Env, channel) {
var ret = [];
Env.user.userObject.findChannels([channel]).forEach(function (id) {
Env.user.userObject.findChannels([channel], true).forEach(function (id) {
var data = Env.user.proxy[UserObject.SHARED_FOLDERS][id] ||
Env.user.userObject.getFileData(id);
ret.push({
data: Env.user.userObject.getFileData(id),
data: data,
userObject: Env.user.userObject
});
});

@ -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',

@ -379,13 +379,14 @@ define([
};
exp.mkIndentSettings = function (metadataMgr) {
var setIndentation = function (units, useTabs, fontSize, spellcheck) {
var setIndentation = function (units, useTabs, fontSize, spellcheck, brackets) {
if (typeof(units) !== 'number') { return; }
var doc = editor.getDoc();
editor.setOption('indentUnit', units);
editor.setOption('tabSize', units);
editor.setOption('indentWithTabs', useTabs);
editor.setOption('spellcheck', spellcheck);
editor.setOption('autoCloseBrackets', brackets);
editor.setOption("extraKeys", {
Tab: function() {
if (doc.somethingSelected()) {
@ -415,11 +416,13 @@ define([
var useTabs = data[useTabsKey];
var fontSize = data[fontKey];
var spellcheck = data[spellcheckKey];
var brackets = data.brackets;
setIndentation(
typeof(indentUnit) === 'number'? indentUnit : 2,
typeof(useTabs) === 'boolean'? useTabs : false,
typeof(fontSize) === 'number' ? fontSize : 12,
typeof(spellcheck) === 'boolean' ? spellcheck : false);
typeof(spellcheck) === 'boolean' ? spellcheck : false,
typeof(brackets) === 'boolean' ? brackets : true);
};
metadataMgr.onChangeLazy(updateIndentSettings);
updateIndentSettings();

@ -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({
@ -450,7 +457,7 @@ define([
path: initialPathInDrive // Where to store the pad if we don't have it in our drive
};
Cryptpad.setPadTitle(data, function (err) {
cb(err);
cb({error: err});
});
});
sframeChan.on('EV_SET_TAB_TITLE', function (newTabTitle) {
@ -488,6 +495,12 @@ define([
});
sframeChan.on('Q_ACCEPT_OWNERSHIP', function (data, cb) {
var parsed = Utils.Hash.parsePadUrl(data.href);
if (parsed.type === 'drive') {
// Shared folder
var secret = Utils.Hash.getSecrets(parsed.type, parsed.hash, data.password);
Cryptpad.addSharedFolder(null, secret, cb);
} else {
var _data = {
password: data.password,
href: data.href,
@ -500,6 +513,7 @@ define([
Cryptpad.setPadTitle(_data, function (err) {
cb({error: err});
});
}
// Also add your mailbox to the metadata object
var padParsed = Utils.Hash.parsePadUrl(data.href);
@ -928,8 +942,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 +1231,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 = {};

@ -64,10 +64,13 @@ define([
sframeChan.query('Q_SET_PAD_TITLE_IN_DRIVE', {
title: title,
defaultTitle: defaultTitle
}, function (err) {
}, function (err, obj) {
err = err || (obj && obj.error);
if (err === 'E_OVER_LIMIT') {
return void UI.alert(Messages.pinLimitNotPinned, null, true);
} else if (err) { return; }
} else if (err) {
return UI.alert(Messages.driveOfflineError);
}
evTitleChange.fire(title);
if (titleUpdated) {
titleUpdated(undefined, title);

@ -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.",
@ -940,7 +941,7 @@
"creation_newPadModalDescription": "Klicke auf einen Pad-Typ, um das entsprechende Pad zu erstellen. Du kannst auch die <b>Tab</b>-Taste für die Auswahl und die <b>Enter</b>-Taste zum Bestätigen benutzen.",
"creation_newPadModalDescriptionAdvanced": "Du kannst das Kästchen markieren (oder den Wert mit der Leertaste ändern), um den Dialog bei der Pad-Erstellung anzuzeigen (für eigene oder auslaufende Dokumente etc.).",
"creation_newPadModalAdvanced": "Dialog bei der Pad-Erstellung anzeigen",
"password_info": "Das Pad, das du öffnen möchtest, ist mit einem Passwort geschützt. Gib das richtige Passwort ein, um den Inhalt anzuzeigen.",
"password_info": "Das Pad, das du öffnen möchtest, existiert nicht mehr oder ist mit einem Passwort geschützt. Gib das richtige Passwort ein, um den Inhalt anzuzeigen.",
"password_error": "Pad nicht gefunden!<br>Dieser Fehler kann zwei Ursachen haben: Entweder ist das Passwort ungültig oder das Pad wurde vom Server gelöscht.",
"password_placeholder": "Gib das Passwort hier ein...",
"password_submit": "Abschicken",
@ -1157,8 +1158,76 @@
"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",
"settings_codeBrackets": "Klammern automatisch schließen",
"team_viewers": "Betrachter",
"drive_sfPassword": "Dein geteilter Ordner {0} ist nicht mehr verfügbar. Entweder wurde er von seinem Eigentümer gelöscht oder er ist nun mit einem neuen Passwort geschützt. Du kannst den Ordner aus deinem CryptDrive entfernen oder den Zugriff durch Eingabe des neuen Passworts wiederherstellen.",
"drive_sfPasswordError": "Falsches Passwort",
"password_error_seed": "Pad nicht gefunden!<br>Dieser Fehler kann zwei Ursachen haben: Entweder wurde ein Passwort gesetzt/geändert oder das Pad wurde vom Server gelöscht.",
"properties_confirmChangeFile": "Bist du sicher? Benutzer, die das neue Passwort nicht kennen, werden den Zugriff auf die Datei verlieren.",
"properties_confirmNewFile": "Bist du sicher? Durch das Hinzufügen eines Passwortes wird sich der Link für die Datei ändern. Benutzer, die das Passwort nicht kennen, werden den Zugriff auf die Datei verlieren.",
"properties_passwordWarningFile": "Das Passwort wurde erfolgreich geändert. Allerdings konnten die Daten in deinem CryptDrive nicht aktualisiert werden. Möglicherweise musst die alte Version der Datei manuell entfernen.",
"properties_passwordSuccessFile": "Das Passwort wurde erfolgreich geändert."
}

@ -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...",
@ -947,7 +948,7 @@
"creation_newPadModalDescription": "Cliquez sur un type de pad pour le créer. Vous pouvez aussi appuyer sur <b>Tab</b> pour sélectionner un type et appuyer sur <b>Entrée</b> pour valider.",
"creation_newPadModalDescriptionAdvanced": "Cochez la case si vous souhaitez voir l'écran de création de pads (pour les pads avec propriétaire ou à durée de vie). Vous pouvez appuyer sur <b>Espace</b> pour changer sa valeur.",
"creation_newPadModalAdvanced": "Afficher l'écran de création de pads",
"password_info": "Le pad auquel vous essayez d'accéder est protégé par un mot de passe. Entrez le bon mot de passe pour accéder à son contenu.",
"password_info": "Le pad auquel vous essayez d'accéder n'existe plus ou est protégé par un mot de passe. Entrez le bon mot de passe pour accéder à son contenu.",
"password_error": "Pad introuvable !<br>Cette erreur peut provenir de deux facteurs. Soit le mot de passe est faux, soit le pad a été supprimé du serveur.",
"password_placeholder": "Tapez le mot de passe ici...",
"password_submit": "Valider",
@ -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,72 @@
"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",
"settings_codeBrackets": "Fermer automatiquement les parenthèses",
"team_viewers": "Lecteurs",
"drive_sfPassword": "Votre dossier partagé {0} n'est plus disponible. Il a soit été supprimé par son propriétaire ou il est protégé par un nouveau mot de passe. Vous pouvez supprimer ce dossier de votre CryptDrive ou retrouver l'accès en tapant le nouveau mot de passe.",
"drive_sfPasswordError": "Mot de passe incorrect",
"password_error_seed": "Pad introuvable !<br>Cette erreur peut provenir de deux facteurs. Soit un mot de passe a été ajouté ou modifié, soit le pad a été supprimé par son propriétaire.",
"properties_confirmChangeFile": "Êtes-vous sûr ? Les utilisateurs ne connaissant pas le nouveau mot de passe perdront l'accès au fichier.",
"properties_confirmNewFile": "Êtes-vous sûr ? Ajouter un mot de passe changera l'URL de ce fichier. Les utilisateurs ne connaissant pas le nouveau mot de passe perdront l'accès au fichier.",
"properties_passwordWarningFile": "Le mot de passe a été modifié avec succès mais nous n'avons pas réussi à mettre à jour votre CryptDrive avec les nouvelles informations. Vous devrez peut-être supprimer manuellement l'ancienne version de ce fichier.",
"properties_passwordSuccessFile": "Le mot de passe a été modifié avec succès.",
"driveOfflineError": "Votre connexion à CryptPad a été perdue. Les modifications dans ce pad ne seront pas stockées dans votre CryptDrive. Veuillez fermer tous vos onglets CryptPad et ré-essayer dans une nouvelle fenêtre. "
}

@ -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.",
@ -964,7 +965,7 @@
"creation_newPadModalDescription": "Click on a pad type to create it. You can also press <b>Tab</b> to select the type and press <b>Enter</b> to confirm.",
"creation_newPadModalDescriptionAdvanced": "You can check the box (or press <b>Space</b> to change its value) if you want to display the pad creation screen (for owned pads, expiring pads, etc.).",
"creation_newPadModalAdvanced": "Display the pad creation screen",
"password_info": "The pad you're trying to open is protected with a password. Enter the correct password to access its content.",
"password_info": "The pad you're trying to open no longer exist or is protected with a password. Enter the correct password to access its content.",
"password_error": "Pad not found!<br>This error can be caused by two factors: either the password in invalid, or the pad has been deleted from the server.",
"password_placeholder": "Type the password here...",
"password_submit": "Submit",
@ -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,72 @@
"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",
"settings_codeBrackets": "Auto-close brackets",
"team_viewers": "Viewers",
"drive_sfPassword": "Your shared folder {0} is no longer available. It has either been deleted by its owner or it is now protected with a new password. You can remove this folder from your CryptDrive, or recover access using the new password.",
"drive_sfPasswordError": "Wrong password",
"password_error_seed": "Pad not found!<br>This error can be caused by two factors: either a password was added/changed, or the pad has been deleted from the server.",
"properties_confirmChangeFile": "Are you sure? Users without the new password will lose access to this file.",
"properties_confirmNewFile": "Are you sure? Adding a password will change this file's URL. Users without the password will lose access to this file.",
"properties_passwordWarningFile": "The password was successfully changed but we were unable to update your CryptDrive with the new data. You may have to remove the old version of the file manually.",
"properties_passwordSuccessFile": "The password was successfully changed.",
"driveOfflineError": "Your connection to CryptPad has been lost. Changes to this pad will not be saved in your CryptDrive. Please close all CryptPad tabs and try again in a new window. "
}

@ -12,7 +12,8 @@
"kanban": "Kanban",
"todo": "A Fazer",
"contacts": "Contactos",
"sheet": "SpreadSheet (Beta)"
"sheet": "SpreadSheet (Beta)",
"teams": ""
},
"button_newpad": "Novo bloco RTF",
"button_newcode": "Novo bloco de código",
@ -335,5 +336,117 @@
},
"feedback_about": "If you're reading this, you were probably curious why CryptPad is requesting web pages when you perform certain actions",
"feedback_privacy": "We care about your privacy, and at the same time we want CryptPad to be very easy to use. We use this file to figure out which UI features matter to our users, by requesting it along with a parameter specifying which action was taken.",
"feedback_optout": "If you would like to opt out, visit <a href='/settings/'>your user settings page</a>, where you'll find a checkbox to enable or disable user feedback"
"feedback_optout": "If you would like to opt out, visit <a href='/settings/'>your user settings page</a>, where you'll find a checkbox to enable or disable user feedback",
"button_newkanban": "",
"button_newsheet": "",
"padNotPinned": "",
"anonymousStoreDisabled": "",
"expiredError": "",
"deletedError": "",
"inactiveError": "",
"chainpadError": "",
"invalidHashError": "",
"errorCopy": "",
"errorRedirectToHome": "",
"newVersionError": "",
"deletedFromServer": "",
"mustLogin": "",
"disabledApp": "",
"realtime_unrecoverableError": "",
"typing": "",
"initializing": "",
"forgotten": "",
"errorState": "",
"userlist_offline": "",
"supportCryptpad": "",
"pinLimitReachedAlertNoAccounts": "",
"moreActions": "",
"importButton": "",
"exportButton": "",
"saveTitle": "",
"forgetButton": "",
"userListButton": "",
"chatButton": "",
"userAccountButton": "",
"uploadButton": "",
"uploadFolderButton": "",
"uploadButtonTitle": "",
"useTemplate": "",
"useTemplateOK": "",
"useTemplateCancel": "",
"template_import": "",
"template_empty": "",
"propertiesButton": "",
"propertiesButtonTitle": "",
"printText": "",
"printButtonTitle2": "",
"printBackground": "",
"printBackgroundButton": "",
"printBackgroundValue": "",
"printBackgroundNoValue": "",
"printBackgroundRemove": "",
"filePickerButton": "",
"filePicker_close": "",
"filePicker_description": "",
"filePicker_filter": "",
"or": "",
"tags_title": "",
"tags_add": "",
"tags_searchHint": "",
"tags_notShared": "",
"tags_duplicate": "",
"tags_noentry": "",
"slideOptionsText": "",
"slide_invalidLess": "",
"languageButton": "",
"themeButton": "",
"themeButtonTitle": "",
"viewOpen": "",
"viewOpenTitle": "",
"getEmbedCode": "",
"viewEmbedTitle": "",
"viewEmbedTag": "",
"fileEmbedTitle": "",
"fileEmbedScript": "",
"fileEmbedTag": "",
"ok": "",
"doNotAskAgain": "",
"show_help_button": "",
"hide_help_button": "",
"help_button": "",
"historyText": "",
"openLinkInNewTab": "",
"pad_mediatagTitle": "",
"pad_mediatagWidth": "",
"pad_mediatagHeight": "",
"pad_mediatagRatio": "",
"pad_mediatagBorder": "",
"pad_mediatagPreview": "",
"pad_mediatagImport": "",
"pad_mediatagOptions": "",
"kanban_newBoard": "",
"kanban_item": "",
"kanban_todo": "",
"kanban_done": "",
"kanban_working": "",
"kanban_deleteBoard": "",
"kanban_addBoard": "",
"kanban_removeItem": "",
"kanban_removeItemConfirm": "",
"poll_remove": "",
"poll_edit": "",
"poll_locked": "",
"poll_unlocked": "",
"poll_total": "",
"poll_comment_list": "",
"poll_comment_add": "",
"poll_comment_submit": "",
"poll_comment_remove": "",
"poll_comment_placeholder": "",
"poll_comment_disabled": "",
"oo_reconnect": "",
"oo_cantUpload": "",
"oo_uploaded": "",
"canvas_opacityLabel": "",
"canvas_widthLabel": ""
}

@ -492,10 +492,13 @@ define([
};
// Get drive ids of files from their channel ids
exp.findChannels = function (channels) {
exp.findChannels = function (channels, includeSharedFolders) {
var allFilesList = files[FILES_DATA];
return getFiles([FILES_DATA]).filter(function (k) {
var data = allFilesList[k];
var sfList = files[SHARED_FOLDERS];
var paths = [FILES_DATA];
if (includeSharedFolders) { paths.push(SHARED_FOLDERS); }
return getFiles(paths).filter(function (k) {
var data = allFilesList[k] || sfList[k] || {};
return channels.indexOf(data.channel) !== -1;
});
};

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

@ -280,7 +280,7 @@
nodeItem.dragfn = element.drag;
nodeItem.dragendfn = element.dragend;
nodeItem.dropfn = element.drop;
__onclickHandler(nodeItem);
__onclickHandler(nodeItemText);
__onColorClickHandler(nodeItem, "item");
board.appendChild(nodeItem);
// send event that board has changed
@ -395,17 +395,15 @@
contentBoard.appendChild(nodeItem);
}
//footer board
var footerBoard = document.createElement('footer');
//add button
var addBoardItem = document.createElement('button');
$(addBoardItem).addClass("kanban-additem btn btn-default fa fa-plus");
footerBoard.appendChild(addBoardItem);
$(addBoardItem).addClass("kanban-title-button btn btn-default fa fa-plus");
headerBoard.appendChild(addBoardItem);
__onAddItemClickHandler(addBoardItem);
//board assembly
boardNode.appendChild(headerBoard);
boardNode.appendChild(contentBoard);
boardNode.appendChild(footerBoard);
//board add
self.container.appendChild(boardNode);
}
@ -509,13 +507,13 @@
var addBoard = document.createElement('div');
addBoard.id = 'kanban-addboard';
addBoard.setAttribute('class', 'fa fa-plus');
boardContainerOuter.appendChild(addBoard);
self.container = boardContainer;
//add boards
self.addBoards(self.options.boards);
//appends to container
self.element.appendChild(boardContainerOuter);
self.element.appendChild(addBoard);
// send event that board has changed
self.onChange();

@ -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',
@ -85,6 +85,7 @@ define([
'code': [
'cp-settings-code-indent-unit',
'cp-settings-code-indent-type',
'cp-settings-code-brackets',
'cp-settings-code-font-size',
'cp-settings-code-spellcheck',
],
@ -1445,6 +1446,35 @@ define([
return $div;
};
create['code-brackets'] = function () {
var key = 'brackets';
var $div = $('<div>', {
'class': 'cp-settings-code-brackets cp-sidebarlayout-element'
});
$('<label>').text(Messages.settings_codeBrackets).appendTo($div);
var $inputBlock = $('<div>', {
'class': 'cp-sidebarlayout-input-block',
}).css('flex-flow', 'column')
.appendTo($div);
var $cbox = $(UI.createCheckbox('cp-settings-codebrackets'));
var $checkbox = $cbox.find('input').on('change', function () {
var val = $checkbox.is(':checked');
if (typeof(val) !== 'boolean') { return; }
common.setAttribute(['codemirror', key], val);
});
$cbox.appendTo($inputBlock);
common.getAttribute(['codemirror', key], function (e, val) {
if (e) { return void console.error(e); }
$checkbox[0].checked = typeof(val) !== "boolean" || val;
});
return $div;
};
create['code-font-size'] = function () {
var key = 'fontSize';

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

@ -26,6 +26,14 @@ define([
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();
@ -170,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;
@ -203,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,188 @@
@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;
}
}
.cp-team-cat-header {
justify-content: center;
.avatar_main(30px);
.cp-avatar {
justify-content: center;
font-size: 20px;
}
media-tag, .cp-avatar-default {
margin-right: 3px;
}
media-tag {
order: -1;
}
cursor: default !important;
font-size: 18px;
&:hover {
background-color: transparent !important;
}
span.cp-sidebarlayout-category-name {
padding-left: 5px;
}
}
.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-avatar img {
max-width: 100%;
max-height: 100%;
}
}
.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;
@ -303,44 +367,69 @@ define([
refreshList(common, cb);
});
makeBlock('create', function (common, cb) {
var refreshCreate = 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 || {}).filter(function (id) {
return privateData.teams[id].owner;
}).length >= Constants.MAX_TEAMS_OWNED; // && !privateData.devMode;
var getWarningBox = function () {
return h('div.alert.alert-warning', {
role:'alert'
}, 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);
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide();
content.push($spinner[0]);
var state = false;
$(button).click(function () {
if (state) { return; }
var name = $(input).val();
if (!name.trim()) { return; }
state = true;
UI.confirm('Are you sure?', function (yes) {
if (!yes) {
state = false;
return;
}
$spinner.show();
APP.module.execCommand('CREATE_TEAM', {
name: name
}, function () {
}, function (obj) {
if (obj && obj.error) {
console.error(obj.error);
$spinner.hide();
return void UI.warn(Messages.error);
}
// Redraw the create block
var $createDiv = $('div.cp-team-create').empty();
isOwner = true;
$createDiv.append(getWarningBox());
// Redraw the teams list
var $div = $('div.cp-team-list').empty();
refreshList(common, function (content) {
state = false;
$div.append(content);
$spinner.hide();
$('div.cp-team-cat-list').click();
});
});
});
});
cb(content);
});
makeBlock('back', function (common, cb) {
refreshList(common, cb);
};
makeBlock('create', function (common, cb) {
refreshCreate(common, cb);
});
makeBlock('drive', function (common, cb) {
@ -374,10 +463,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,52 +476,106 @@ 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';
var todo = function () {
var role = ROLES[theirRole - 1] || 'MEMBER';
$(demote).hide();
describeUser(common, data, demote);
describeUser(common, data.curvePublic, {
role: role
}, promote);
};
if (isMe) {
return void UI.confirm(Messages.team_demoteMeConfirm, function (yes) {
if (!yes) { return; }
todo();
});
}
todo();
});
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();
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) {
@ -443,6 +586,7 @@ define([
redrawRoster(common);
});
});
});
$actions.append(remove);
}
@ -450,7 +594,8 @@ define([
var content = [
avatar,
name,
actions
actions,
status,
];
var div = h('div.cp-team-roster-member', {
title: data.displayName
@ -471,9 +616,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 +632,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 +667,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 +683,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 +711,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 +755,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 +846,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 +903,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 +920,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,
@ -752,10 +960,17 @@ define([
metadataMgr.onChange(function () {
var $div = $('div.cp-team-list');
if (!$div.length) { return; }
if ($div.length) {
refreshList(common, function (content) {
$div.empty().append(content);
});
}
var $divCreate = $('div.cp-team-create');
if ($divCreate.length) {
refreshCreate(common, function (content) {
$divCreate.empty().append(content);
});
}
});
var onDisconnect = function (noAlert) {

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