Merge branch 'master' of github.com:xwiki-labs/cryptpad
commit
5b22406dd7
79
CHANGELOG.md
79
CHANGELOG.md
|
@ -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 {
|
||||
width: 100%;
|
||||
}
|
||||
.cp-creation-team {
|
||||
.avatar_main(25px);
|
||||
width: 140px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
cursor: default;
|
||||
font: @colortheme_app-font;
|
||||
color: @colortheme_modal-fg;
|
||||
margin: 0 1px;
|
||||
|
||||
.tools_unselectable();
|
||||
|
||||
&.cp-selected {
|
||||
background-color: @colortheme_alertify-primary;
|
||||
color: @colortheme_alertify-primary-text;
|
||||
}
|
||||
.cp-creation-team-avatar {
|
||||
.fa {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
.cp-creation-team-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
}
|
||||
border: 1px solid @colortheme_alertify-primary;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -242,7 +242,7 @@
|
|||
margin-left: -2px;
|
||||
}
|
||||
.cp-app-drive-tree-docs {
|
||||
margin-top: 20px;
|
||||
margin-top: 15px;
|
||||
//padding: 0 0 0 20px;
|
||||
padding: 0;
|
||||
cursor: auto;
|
||||
|
@ -269,6 +269,12 @@
|
|||
cursor: pointer;
|
||||
margin-left: -5px;
|
||||
padding-left: 5px;
|
||||
.fa, .cptools {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
width: 25px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,6 +121,9 @@
|
|||
margin: 5px 0px;
|
||||
height: 1px;
|
||||
background: #bbb;
|
||||
& + hr {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
@import (reference) "./app-noscroll.less";
|
||||
@import (reference) "./messenger.less";
|
||||
@import (reference) "./cursor.less";
|
||||
@import (reference) "./usergrid.less";
|
||||
|
||||
.framework_main(@bg-color, @warn-color, @color) {
|
||||
--LessLoader_require: LessLoader_currentFile();
|
||||
|
@ -40,6 +41,7 @@
|
|||
.password_main();
|
||||
.messenger_main();
|
||||
.cursor_main();
|
||||
.usergrid_main();
|
||||
.creation_main(
|
||||
@bg-color: @bg-color,
|
||||
@color: @color
|
||||
|
@ -73,6 +75,7 @@
|
|||
.tippy_main();
|
||||
.checkmark_main(20px);
|
||||
.password_main();
|
||||
.usergrid_main();
|
||||
font: @colortheme_app-font;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
.cp-icon-color-sheet { color: @colortheme_oocell-bg; }
|
||||
.cp-icon-color-kanban { color: @colortheme_kanban-bg; }
|
||||
.cp-icon-color-admin { color: @colortheme_admin-bg; }
|
||||
.cp-icon-color-teams { color: @colortheme_teams-bg; }
|
||||
|
||||
.cp-border-color-pad { border-color: @colortheme_pad-bg !important; }
|
||||
.cp-border-color-code { border-color: @colortheme_code-bg !important; }
|
||||
|
@ -39,5 +40,6 @@
|
|||
.cp-border-color-sheet { border-color: @colortheme_oocell-bg !important; }
|
||||
.cp-border-color-kanban { border-color: @colortheme_kanban-bg !important; }
|
||||
.cp-border-color-admin { border-color: @colortheme_admin-bg !important; }
|
||||
.cp-border-color-teams { border-color: @colortheme_teams-bg !important; }
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,16 @@
|
|||
}
|
||||
.leftside-menu-category_main() {
|
||||
.unselectable_make();
|
||||
padding: 5px 20px;
|
||||
padding: 5px 15px;
|
||||
margin: 15px 0;
|
||||
cursor: pointer;
|
||||
height: @variables_bar-height;
|
||||
line-height: @variables_bar-height - 10px;
|
||||
.fa, .cptools {
|
||||
width: 25px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
min-width: 25px;
|
||||
}
|
||||
&:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
|
|
|
@ -53,12 +53,22 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.cp-limit-upgrade {
|
||||
padding: 0;
|
||||
line-height: 25px;
|
||||
height: 25px;
|
||||
margin: 0 3px;
|
||||
border-radius: 0;
|
||||
.cp-limit-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
justify-content: space-evenly;
|
||||
a {
|
||||
height: 25px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
width: 50%;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,9 +31,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",
|
||||
|
|
2
rpc.js
2
rpc.js
|
@ -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) {
|
||||
$sel[0].scrollIntoView();
|
||||
var _top = $sel[0].getBoundingClientRect().top;
|
||||
var _topContent = $content[0].getBoundingClientRect().top;
|
||||
if ((_topContent + s + $content.height() - 20) < _top) {
|
||||
$sel[0].scrollIntoView();
|
||||
} else {
|
||||
$content.scrollTop(s);
|
||||
}
|
||||
} else {
|
||||
$content.scrollTop(s);
|
||||
}
|
||||
|
@ -3721,6 +3738,10 @@ define([
|
|||
data.roHref = base + data.roHref;
|
||||
}
|
||||
|
||||
if (currentPath[0] === TEMPLATE) {
|
||||
data.isTemplate = true;
|
||||
}
|
||||
|
||||
if (manager.isSharedFolder(el)) {
|
||||
delete data.roHref;
|
||||
//data.noPassword = true;
|
||||
|
@ -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; }
|
||||
toolbar['chat'].find('button').addClass('cp-toolbar-notification');
|
||||
if (!toolbar['chat'].find('button').hasClass('cp-toolbar-button-active')) {
|
||||
toolbar['chat'].find('button').addClass('cp-toolbar-notification');
|
||||
}
|
||||
if (!toolbar['chat'].hasClass('cp-leftside-active')) {
|
||||
toolbar['chat'].find('span.fa').addClass('cp-team-chat-notification');
|
||||
}
|
||||
};
|
||||
|
||||
var notify = function (id) {
|
||||
|
@ -568,12 +572,15 @@ define([
|
|||
|
||||
var el_message = markup.message(message);
|
||||
|
||||
common.notify();
|
||||
if (message.type === 'MSG') {
|
||||
var name = typeof message.name !== "undefined" ?
|
||||
(message.name || Messages.anonymous) :
|
||||
contactsData[message.author].displayName;
|
||||
common.notify({title: name, msg: message.text});
|
||||
common.notify({
|
||||
title: name,
|
||||
msg: message.text,
|
||||
force: toolbar && toolbar.team && !toolbar['chat'].hasClass('cp-leftside-active')
|
||||
});
|
||||
}
|
||||
notifyToolbar();
|
||||
|
||||
|
@ -716,7 +723,7 @@ define([
|
|||
if (room.isFriendChat) {
|
||||
$parentEl = $userlist.find('.cp-app-contacts-friends');
|
||||
} else if (room.isTeamChat) {
|
||||
$parentEl = $userlist.find('.cp-app-contacts-padchat'); // XXX
|
||||
$parentEl = $userlist.find('.cp-app-contacts-padchat');
|
||||
} else if (room.isPadChat) {
|
||||
$parentEl = $userlist.find('.cp-app-contacts-padchat');
|
||||
} else {
|
||||
|
@ -829,7 +836,7 @@ define([
|
|||
return void console.error('Invalid team chat');
|
||||
}
|
||||
var room = rooms[0];
|
||||
room.name = 'TEAMS'; // XXX
|
||||
room.name = Messages.type.team;
|
||||
rooms.forEach(initializeRoom);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -216,6 +216,9 @@ define([
|
|||
// if not archived, add handlers
|
||||
if (!content.archived) {
|
||||
content.handler = function () {
|
||||
if (msg.content.teamChannel) {
|
||||
return void UIElements.displayAddTeamOwnerModal(common, data);
|
||||
}
|
||||
UIElements.displayAddOwnerModal(common, data);
|
||||
};
|
||||
}
|
||||
|
@ -261,8 +264,7 @@ define([
|
|||
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
|
||||
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name']) || '');
|
||||
content.getFormatText = function () {
|
||||
var text = name + " has invited you to join the team <b>" + teamName +"</b>";
|
||||
// XXX
|
||||
var text = Messages._getKey('team_invitedToTeam', [name, teamName]);
|
||||
return text;
|
||||
};
|
||||
if (!content.archived) {
|
||||
|
@ -280,8 +282,7 @@ define([
|
|||
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
|
||||
var teamName = Util.fixHTML(Util.find(msg, ['content', 'teamName']) || '');
|
||||
content.getFormatText = function () {
|
||||
var text = name + " has kicked you from join the team <b>" + teamName +"</b>";
|
||||
// XXX
|
||||
var text = Messages._getKey('team_kickedFromTeam', [name, teamName]);
|
||||
return text;
|
||||
};
|
||||
if (!content.archived) {
|
||||
|
@ -296,10 +297,9 @@ define([
|
|||
// Display the notification
|
||||
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
|
||||
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name']) || '');
|
||||
//var key = 'owner_request_' + (msg.content.answer ? 'accepted' : 'declined');
|
||||
var key = 'team_' + (msg.content.answer ? 'accept' : 'decline') + 'Invitation';
|
||||
content.getFormatText = function () {
|
||||
//return Messages._getKey(key, [name, title]); // XXX
|
||||
return name +' has ' + (msg.content.answer ? 'accepted' : 'declined') + ' your offer to join the team <b>' + teamName + '</b>';
|
||||
return Messages._getKey(key, [name, teamName]);
|
||||
};
|
||||
if (!content.archived) {
|
||||
content.dismissHandler = defaultDismiss(common, data);
|
||||
|
|
|
@ -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'}); }
|
||||
|
||||
store.rpc.unpin(data, function (e, hash) {
|
||||
var s = getStore(data && data.teamId);
|
||||
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
|
||||
|
||||
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('DRIVE_CHANGE', {
|
||||
path: ['drive', UserObject.FILES_DATA]
|
||||
}, clientId);
|
||||
// Send a CHANGE events to all the teams because we may have just
|
||||
// added a pad to a shared folder stored in multiple teams
|
||||
getAllStores().forEach(function (_s) {
|
||||
var send = _s.id ? _s.sendEvent : sendDriveEvent;
|
||||
send('DRIVE_CHANGE', {
|
||||
path: ['drive', UserObject.FILES_DATA]
|
||||
}, clientId);
|
||||
});
|
||||
onSync(data.teamId, cb);
|
||||
});
|
||||
};
|
||||
|
@ -606,19 +636,68 @@ define([
|
|||
// No password for profile
|
||||
list.push(Hash.hrefToHexChannelId('/profile/#' + store.proxy.profile.edit, null));
|
||||
}
|
||||
if (store.proxy.mailboxes) {
|
||||
Object.keys(store.proxy.mailboxes || {}).forEach(function (id) {
|
||||
if (id === 'supportadmin') { return; }
|
||||
var m = store.proxy.mailboxes[id];
|
||||
list.push(m.channel);
|
||||
});
|
||||
}
|
||||
if (store.proxy.teams) {
|
||||
Object.keys(store.proxy.teams || {}).forEach(function (id) {
|
||||
var t = store.proxy.teams[id];
|
||||
if (t.owner) {
|
||||
list.push(t.channel);
|
||||
list.push(t.keys.roster.channel);
|
||||
list.push(t.keys.chat.channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
return list;
|
||||
};
|
||||
var removeOwnedPads = function (waitFor) {
|
||||
// Delete owned pads
|
||||
var edPublic = Util.find(store, ['proxy', 'edPublic']);
|
||||
var ownedPads = getOwnedPads();
|
||||
var sem = Saferphore.create(10);
|
||||
ownedPads.forEach(function (c) {
|
||||
var w = waitFor();
|
||||
sem.take(function (give) {
|
||||
Store.removeOwnedChannel(null, c, give(function (obj) {
|
||||
if (obj && obj.error) { console.error(obj.error); }
|
||||
var otherOwners = false;
|
||||
nThen(function (_w) {
|
||||
Store.anonRpcMsg(null, {
|
||||
msg: 'GET_METADATA',
|
||||
data: c
|
||||
}, _w(function (obj) {
|
||||
if (obj && obj.error) {
|
||||
give();
|
||||
return void _w.abort();
|
||||
}
|
||||
var md = obj[0];
|
||||
var isOwner = md && Array.isArray(md.owners) && md.owners.indexOf(edPublic) !== -1;
|
||||
if (!isOwner) {
|
||||
give();
|
||||
return void _w.abort();
|
||||
}
|
||||
otherOwners = md.owners.some(function (ed) { return ed !== edPublic; });
|
||||
}));
|
||||
}).nThen(function (_w) {
|
||||
if (otherOwners) {
|
||||
Store.setPadMetadata(null, {
|
||||
channel: c,
|
||||
command: 'RM_OWNERS',
|
||||
value: [edPublic],
|
||||
}, _w());
|
||||
return;
|
||||
}
|
||||
// We're the only owner: delete the pad
|
||||
store.rpc.removeOwnedChannel(c, _w(function (err) {
|
||||
if (err) { console.error(err); }
|
||||
}));
|
||||
}).nThen(function () {
|
||||
give();
|
||||
w();
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -758,17 +837,6 @@ define([
|
|||
* - attr (Array)
|
||||
* - value (String)
|
||||
*/
|
||||
var getAllStores = function () {
|
||||
var stores = [store];
|
||||
var teamModule = store.modules['team'];
|
||||
if (teamModule) {
|
||||
var teams = teamModule.getTeams().map(function (id) {
|
||||
return teamModule.getTeam(id);
|
||||
});
|
||||
Array.prototype.push.apply(stores, teams);
|
||||
}
|
||||
return stores;
|
||||
};
|
||||
Store.setPadAttribute = function (clientId, data, cb) {
|
||||
nThen(function (waitFor) {
|
||||
getAllStores().forEach(function (s) {
|
||||
|
@ -931,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('DRIVE_CHANGE', {
|
||||
path: ['drive', UserObject.FILES_DATA]
|
||||
}, clientId);
|
||||
// Send the CHANGE event to all the stores because the command may have
|
||||
// affected data from a shared folder used by multiple teams.
|
||||
getAllStores().forEach(function (_s) {
|
||||
var send = _s.id ? _s.sendEvent : sendDriveEvent;
|
||||
send('DRIVE_CHANGE', {
|
||||
path: ['drive', UserObject.FILES_DATA]
|
||||
}, clientId);
|
||||
});
|
||||
onSync(cmdData.teamId, function () {
|
||||
cb(data2);
|
||||
});
|
||||
|
@ -1922,7 +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,20 +2090,20 @@ define([
|
|||
loadUniversal(Team, 'team', waitFor);
|
||||
cleanFriendRequests();
|
||||
}).nThen(function () {
|
||||
arePinsSynced(function (err, yes) {
|
||||
if (!yes) {
|
||||
resetPins(function (err) {
|
||||
if (err) { return console.error(err); }
|
||||
console.log('RESET DONE');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var requestLogin = function () {
|
||||
broadcast([], "REQUEST_LOGIN");
|
||||
};
|
||||
|
||||
if (store.loggedIn) {
|
||||
arePinsSynced(function (err, yes) {
|
||||
if (!yes) {
|
||||
resetPins(function (err) {
|
||||
if (err) { return console.error(err); }
|
||||
console.log('RESET DONE');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* This isn't truly secure, since anyone who can read the user's object can
|
||||
set their local loginToken to match that in the object. However, it exposes
|
||||
a UI that will work most of the time. */
|
||||
|
@ -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,18 +495,25 @@ define([
|
|||
});
|
||||
|
||||
sframeChan.on('Q_ACCEPT_OWNERSHIP', function (data, cb) {
|
||||
var _data = {
|
||||
password: data.password,
|
||||
href: data.href,
|
||||
channel: data.channel,
|
||||
title: data.title,
|
||||
owners: data.metadata.owners,
|
||||
expire: data.metadata.expire,
|
||||
forceSave: true
|
||||
};
|
||||
Cryptpad.setPadTitle(_data, function (err) {
|
||||
cb({error: err});
|
||||
});
|
||||
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,
|
||||
channel: data.channel,
|
||||
title: data.title,
|
||||
owners: data.metadata.owners,
|
||||
expire: data.metadata.expire,
|
||||
forceSave: true
|
||||
};
|
||||
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. "
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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 (obj) {
|
||||
if (obj && obj.error) {
|
||||
console.error(obj.error);
|
||||
$spinner.hide();
|
||||
return void UI.warn(Messages.error);
|
||||
}
|
||||
APP.module.execCommand('CREATE_TEAM', {
|
||||
name: name
|
||||
}, function () {
|
||||
var $div = $('div.cp-team-list').empty();
|
||||
refreshList(common, function (content) {
|
||||
state = false;
|
||||
$div.append(content);
|
||||
$('div.cp-team-cat-list').click();
|
||||
});
|
||||
// Redraw the create block
|
||||
var $createDiv = $('div.cp-team-create').empty();
|
||||
isOwner = true;
|
||||
$createDiv.append(getWarningBox());
|
||||
// Redraw the teams list
|
||||
var $div = $('div.cp-team-list').empty();
|
||||
refreshList(common, function (content) {
|
||||
state = false;
|
||||
$div.append(content);
|
||||
$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,60 +476,115 @@ define([
|
|||
redrawRoster(common);
|
||||
});
|
||||
};
|
||||
var makeMember = function (common, data, me) {
|
||||
var makeMember = function (common, data, me, roster) {
|
||||
if (!data.curvePublic) { return; }
|
||||
|
||||
var otherOwners = Object.keys(roster || {}).some(function (key) {
|
||||
var user = roster[key];
|
||||
return user.role === "OWNER" && user.curvePublic !== me.curvePublic && !user.pendingOwner;
|
||||
});
|
||||
|
||||
// Avatar
|
||||
var avatar = h('span.cp-avatar.cp-team-member-avatar');
|
||||
common.displayAvatar($(avatar), data.avatar, data.displayName);
|
||||
// Name
|
||||
var name = h('span.cp-team-member-name', data.displayName);
|
||||
if (data.pendingOwner) {
|
||||
$(name).append(h('em', {
|
||||
title: Messages.team_pendingOwnerTitle
|
||||
}, ' ' + Messages.team_pendingOwner));
|
||||
}
|
||||
// Status
|
||||
var status = h('span.cp-team-member-status'+(data.online ? '.online' : ''));
|
||||
// Actions
|
||||
var actions = h('span.cp-team-member-actions');
|
||||
var $actions = $(actions);
|
||||
var isMe = me && me.curvePublic === data.curvePublic;
|
||||
var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1;
|
||||
var theirRole = ROLES.indexOf(data.role) || 0;
|
||||
// If they're an admin and I am an owner, I can promote them to owner
|
||||
if (!isMe && myRole > theirRole && theirRole === 1 && !data.pending) {
|
||||
var promoteOwner = h('span.fa.fa-angle-double-up', {
|
||||
title: Messages.team_rosterPromoteOwner
|
||||
});
|
||||
$(promoteOwner).click(function () {
|
||||
UI.confirm(Messages.team_ownerConfirm, function (yes) {
|
||||
if (!yes) { return; }
|
||||
$(promoteOwner).hide();
|
||||
APP.module.execCommand('OFFER_OWNERSHIP', {
|
||||
teamId: APP.team,
|
||||
curvePublic: data.curvePublic
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) {
|
||||
console.error(obj.error);
|
||||
return void UI.warn(Messages.error);
|
||||
}
|
||||
UI.log(Messages.sent);
|
||||
});
|
||||
});
|
||||
});
|
||||
$actions.append(promoteOwner);
|
||||
}
|
||||
// If they're a member and I have a higher role than them, I can promote them to admin
|
||||
if (!isMe && myRole > theirRole && theirRole === 0) {
|
||||
if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) {
|
||||
var promote = h('span.fa.fa-angle-double-up', {
|
||||
title: 'Promote' // XXX
|
||||
title: Messages.team_rosterPromote
|
||||
});
|
||||
$(promote).click(function () {
|
||||
data.role = 'ADMIN';
|
||||
$(promote).hide();
|
||||
describeUser(common, data, promote);
|
||||
describeUser(common, data.curvePublic, {
|
||||
role: 'ADMIN'
|
||||
}, promote);
|
||||
});
|
||||
$actions.append(promote);
|
||||
}
|
||||
// If I'm not a member and I have an equal or higher role than them, I can demote them
|
||||
// (if they're not already a MEMBER)
|
||||
if (!isMe && myRole >= theirRole && theirRole > 0) {
|
||||
if (myRole >= theirRole && theirRole > 0 && !data.pending) {
|
||||
var demote = h('span.fa.fa-angle-double-down', {
|
||||
title: 'Demote' // XXX
|
||||
title: Messages.team_rosterDemote
|
||||
});
|
||||
$(demote).click(function () {
|
||||
data.role = ROLES[theirRole - 1] || 'MEMBER';
|
||||
$(demote).hide();
|
||||
describeUser(common, data, demote);
|
||||
var todo = function () {
|
||||
var role = ROLES[theirRole - 1] || 'MEMBER';
|
||||
$(demote).hide();
|
||||
describeUser(common, data.curvePublic, {
|
||||
role: role
|
||||
}, promote);
|
||||
};
|
||||
if (isMe) {
|
||||
return void UI.confirm(Messages.team_demoteMeConfirm, function (yes) {
|
||||
if (!yes) { return; }
|
||||
todo();
|
||||
});
|
||||
}
|
||||
todo();
|
||||
});
|
||||
$actions.append(demote);
|
||||
if (!(isMe && myRole === 2 && !otherOwners)) {
|
||||
$actions.append(demote);
|
||||
}
|
||||
}
|
||||
// If I'm not a member and I have an equal or higher role than them, I can remove them
|
||||
if (!isMe && myRole > 0 && myRole >= theirRole) {
|
||||
// Note: we can't remove owners, we have to demote them first
|
||||
if (!isMe && myRole > 0 && myRole >= theirRole && theirRole !== 2) {
|
||||
var remove = h('span.fa.fa-times', {
|
||||
title: 'Remove' // XXX
|
||||
title: Messages.team_rosterKick
|
||||
});
|
||||
$(remove).click(function () {
|
||||
$(remove).hide();
|
||||
APP.module.execCommand('REMOVE_USER', {
|
||||
teamId: APP.team,
|
||||
curvePublic: data.curvePublic,
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) {
|
||||
$(remove).show();
|
||||
return void UI.alert(Messages.error);
|
||||
}
|
||||
redrawRoster(common);
|
||||
UI.confirm(Messages._getKey('team_kickConfirm', [Util.fixHTML(data.displayName)]), function (yes) {
|
||||
if (!yes) { return; }
|
||||
APP.module.execCommand('REMOVE_USER', {
|
||||
pending: data.pending,
|
||||
teamId: APP.team,
|
||||
curvePublic: data.curvePublic,
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) {
|
||||
$(remove).show();
|
||||
return void UI.alert(Messages.error);
|
||||
}
|
||||
redrawRoster(common);
|
||||
});
|
||||
});
|
||||
});
|
||||
$actions.append(remove);
|
||||
|
@ -450,7 +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; }
|
||||
refreshList(common, function (content) {
|
||||
$div.empty().append(content);
|
||||
});
|
||||
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…
Reference in New Issue