diff --git a/CHANGELOG.md b/CHANGELOG.md index af83d148f..714d02d59 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/customize.dist/login.js b/customize.dist/login.js index 21668140f..0bf36b7ce 100644 --- a/customize.dist/login.js +++ b/customize.dist/login.js @@ -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', diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 43702f7cd..98a8eff43 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.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)") ]); }; diff --git a/customize.dist/pages/index.js b/customize.dist/pages/index.js index 4f233e718..300637748 100644 --- a/customize.dist/pages/index.js +++ b/customize.dist/pages/index.js @@ -97,7 +97,7 @@ define([ ]);*/ var _link = h('a', { - href: "https://opencollective.com/cryptpad/contribute", + href: "https://opencollective.com/cryptpad/", target: '_blank', rel: 'noopener', }); diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index 6662a9df7..bbc87f9c7 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -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; - } - } } } diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index 24959179c..57d0a84f7 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -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; diff --git a/customize.dist/src/less2/include/creation.less b/customize.dist/src/less2/include/creation.less index 281e15e44..ccf4ce5b1 100644 --- a/customize.dist/src/less2/include/creation.less +++ b/customize.dist/src/less2/include/creation.less @@ -2,6 +2,7 @@ @import (reference) "./colortheme-all.less"; @import (reference) "./tools.less"; @import (reference) './icon-colors.less'; +@import (reference) "./avatar.less"; .creation_vars( @color: @colortheme_default-color, @@ -62,7 +63,7 @@ outline: none; width: 700px; max-width: 90vw; - height: 500px; + height: 550px; max-height: calc(~"100vh - 20px"); margin: 50px; flex-shrink: 0; @@ -175,15 +176,47 @@ color: @colortheme_form-color; } - .cp-creation-team { - .cp-dropdown-container { + .cp-creation-teams { + display: none !important; + .cp-creation-teams-grid { + display: flex; + flex-wrap: wrap; + padding: 0 2px; flex: 1; - min-width: 0; - margin-left: 10px; - margin-right: 10px; - button, .cp-dropdown-content { + } + .cp-creation-team { + .avatar_main(25px); + width: 140px; + height: 35px; + display: flex; + justify-content: center; + align-items: center; + padding: 5px; + cursor: default; + font: @colortheme_app-font; + color: @colortheme_modal-fg; + margin: 0 1px; + + .tools_unselectable(); + + &.cp-selected { + background-color: @colortheme_alertify-primary; + color: @colortheme_alertify-primary-text; + } + .cp-creation-team-avatar { + .fa { + font-size: 25px; + } + } + .cp-creation-team-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; width: 100%; + text-align: center; + line-height: 18px; } + border: 1px solid @colortheme_alertify-primary; } } diff --git a/customize.dist/src/less2/include/drive.less b/customize.dist/src/less2/include/drive.less index 67cc3932b..0691ffd9e 100644 --- a/customize.dist/src/less2/include/drive.less +++ b/customize.dist/src/less2/include/drive.less @@ -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; + } } } } diff --git a/customize.dist/src/less2/include/dropdown.less b/customize.dist/src/less2/include/dropdown.less index 7c316a33a..10044528e 100644 --- a/customize.dist/src/less2/include/dropdown.less +++ b/customize.dist/src/less2/include/dropdown.less @@ -121,6 +121,9 @@ margin: 5px 0px; height: 1px; background: #bbb; + & + hr { + display: none; + } } p { diff --git a/customize.dist/src/less2/include/framework.less b/customize.dist/src/less2/include/framework.less index ef6cee1f1..685bfcaf5 100644 --- a/customize.dist/src/less2/include/framework.less +++ b/customize.dist/src/less2/include/framework.less @@ -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; } diff --git a/customize.dist/src/less2/include/icon-colors.less b/customize.dist/src/less2/include/icon-colors.less index 5c4909628..776a7c443 100644 --- a/customize.dist/src/less2/include/icon-colors.less +++ b/customize.dist/src/less2/include/icon-colors.less @@ -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; } } diff --git a/customize.dist/src/less2/include/leftside-menu.less b/customize.dist/src/less2/include/leftside-menu.less index 1c0b828dd..4ffd2c4b8 100644 --- a/customize.dist/src/less2/include/leftside-menu.less +++ b/customize.dist/src/less2/include/leftside-menu.less @@ -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); diff --git a/customize.dist/src/less2/include/limit-bar.less b/customize.dist/src/less2/include/limit-bar.less index 3ed7bd454..fe5049249 100644 --- a/customize.dist/src/less2/include/limit-bar.less +++ b/customize.dist/src/less2/include/limit-bar.less @@ -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; + } } } } diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index b25cbf43c..24228cfaf 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -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; diff --git a/customize.dist/src/less2/include/usergrid.less b/customize.dist/src/less2/include/usergrid.less new file mode 100644 index 000000000..a2d2c8fc5 --- /dev/null +++ b/customize.dist/src/less2/include/usergrid.less @@ -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; + } + } + } + } +} + diff --git a/customize.dist/translations/messages.ja.js b/customize.dist/translations/messages.ja.js new file mode 100644 index 000000000..7293bfc50 --- /dev/null +++ b/customize.dist/translations/messages.ja.js @@ -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; +}); + diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 34d114402..a54687560 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -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; } diff --git a/historyKeeper.js b/historyKeeper.js index d90b8c113..53ce46807 100644 --- a/historyKeeper.js +++ b/historyKeeper.js @@ -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"); diff --git a/lib/client/index.js b/lib/client/index.js index 8faf8f4a2..3e4bc1225 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -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; diff --git a/lib/metadata.js b/lib/metadata.js index 6231ea224..de40043af 100644 --- a/lib/metadata.js +++ b/lib/metadata.js @@ -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; }; diff --git a/package.json b/package.json index d47aa5a2e..aaa60d36d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/rpc.js b/rpc.js index 5a97cd572..d4bd41c9c 100644 --- a/rpc.js +++ b/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 */ diff --git a/scripts/check-account-deletion.js b/scripts/check-account-deletion.js index 020e3c254..cf271a1da 100644 --- a/scripts/check-account-deletion.js +++ b/scripts/check-account-deletion.js @@ -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'); diff --git a/scripts/evict-inactive.js b/scripts/evict-inactive.js index ebb65d637..b37272e0a 100644 --- a/scripts/evict-inactive.js +++ b/scripts/evict-inactive.js @@ -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(); diff --git a/scripts/generate-admin-keys.js b/scripts/generate-admin-keys.js index 26d26537e..f99c756d7 100644 --- a/scripts/generate-admin-keys.js +++ b/scripts/generate-admin-keys.js @@ -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."); diff --git a/scripts/tests/test-rpc.js b/scripts/tests/test-rpc.js index 178a07c0d..70ea13053 100644 --- a/scripts/tests/test-rpc.js +++ b/scripts/tests/test-rpc.js @@ -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) { diff --git a/storage/tasks.js b/storage/tasks.js index 4db1c7642..2209b3d59 100644 --- a/storage/tasks.js +++ b/storage/tasks.js @@ -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; diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index a1c1ee779..10e178308 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -18,5 +18,10 @@ display: flex; flex-flow: column; + + .cp-support-container { + display: flex; + flex-flow: column; + } } diff --git a/www/admin/inner.js b/www/admin/inner.js index 4c335dd12..7478442c5 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -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); diff --git a/www/code/app-code.less b/www/code/app-code.less index f6c3aa311..f07ef534d 100644 --- a/www/code/app-code.less +++ b/www/code/app-code.less @@ -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(); diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index 4bfe1383a..72897680c 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -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; }); diff --git a/www/common/common-constants.js b/www/common/common-constants.js index 752298841..a083ceb90 100644 --- a/www/common/common-constants.js +++ b/www/common/common-constants.js @@ -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 diff --git a/customize.dist/credential.js b/www/common/common-credential.js similarity index 80% rename from customize.dist/credential.js rename to www/common/common-credential.js index cdbd835c5..ffbc482d6 100644 --- a/customize.dist/credential.js +++ b/www/common/common-credential.js @@ -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); + }); + } +}()); diff --git a/www/common/common-hash.js b/www/common/common-hash.js index e59604136..fa9feb7e1 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -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', diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js index 8eec5c4a3..91322f526 100644 --- a/www/common/common-messaging.js +++ b/www/common/common-messaging.js @@ -81,10 +81,13 @@ define([ }; Msg.updateMyData = function (store, curve) { - var myData = createData(store.proxy); + var myData = createData(store.proxy, false); if (store.proxy.friends) { store.proxy.friends.me = myData; } + if (store.modules['team']) { + store.modules['team'].updateMyData(myData); + } var todo = function (friend) { if (!friend || !friend.notifications) { return; } myData.channel = friend.channel; diff --git a/www/common/common-messenger.js b/www/common/common-messenger.js deleted file mode 100644 index c02d9d4eb..000000000 --- a/www/common/common-messenger.js +++ /dev/null @@ -1,1052 +0,0 @@ -define([ - '/bower_components/chainpad-crypto/crypto.js', - '/common/common-hash.js', - '/common/common-util.js', - '/common/common-realtime.js', - '/common/common-constants.js', - '/customize/messages.js', - - '/bower_components/nthen/index.js', -], function (Crypto, Hash, Util, Realtime, Constants, Messages, nThen) { - 'use strict'; - var Curve = Crypto.Curve; - - var Msg = { - inputs: [], - }; - - var Types = { - message: 'MSG', - update: 'UPDATE', - unfriend: 'UNFRIEND', - mapId: 'MAP_ID', - mapIdAck: 'MAP_ID_ACK' - }; - - var clone = function (o) { - return JSON.parse(JSON.stringify(o)); - }; - - var convertToUint8 = function (obj) { - var l = Object.keys(obj).length; - var u = new Uint8Array(l); - for (var i = 0; i we asked for an invalid last known hash - if (parsed.error && parsed.error === "EINVAL") { - setChannelHead(parsed.channel, '', function () { - getChannelMessagesSince(getChannel(parsed.channel), {}, {}); - }); - return; - } - - // End of initial history - if (parsed.state && parsed.state === 1 && parsed.channel) { - // parsed.channel is Ready - // channel[parsed.channel].ready(); - channels[parsed.channel].ready = true; - onChannelReady(parsed.channel); - return; - } - } - // Initial history message - var chan = parsed[3]; - if (!chan || !channels[chan]) { return; } - pushMsg(channels[chan], parsed[4]); - }; - - var onMessage = function (msg, sender, chan) { - if (!channels[chan.id]) { return; } - - var isMessage = pushMsg(channels[chan.id], msg); - if (isMessage) { - if (channels[chan.id].wc.myID !== sender) { - // Don't notify for your own messages - //channels[chan.id].notify(); - } - //channels[chan.id].refresh(); - } - }; - - // listen for messages... - network.on('message', function(msg, sender) { - onDirectMessage(msg, sender); - }); - - var removeFriend = function (curvePublic, _cb) { - var cb = Util.once(_cb); - if (typeof(cb) !== 'function') { throw new Error('NO_CALLBACK'); } - var data = getFriend(proxy, curvePublic); - - if (!data) { - // friend is not valid - console.error('friend is not valid'); - return void cb({error: 'INVALID_FRIEND'}); - } - - var channel = channels[data.channel]; - if (!channel) { - return void cb({error: "NO_SUCH_CHANNEL"}); - } - - if (!network.webChannels.some(function (wc) { - return wc.id === channel.id; - })) { - console.error('bad channel: ', curvePublic); - } - - var msg = [Types.unfriend, proxy.curvePublic, +new Date()]; - var msgStr = JSON.stringify(msg); - var cryptMsg = channel.encrypt(msgStr); - - try { - if (store.mailbox && data.curvePublic && data.notifications) { - store.mailbox.sendTo('UNFRIEND', { - curvePublic: proxy.curvePublic - }, { - channel: data.notifications, - curvePublic: data.curvePublic - }, function (obj) { - console.log(obj); - if (obj && obj.error) { - return void cb(obj); - } - removeFromFriendList(curvePublic, function () { - delete channels[channel.id]; - emit('UNFRIEND', { - curvePublic: curvePublic, - fromMe: true - }); - cb(); - }); - }); - } else { - removeFromFriendList(curvePublic, function () { - delete channels[channel.id]; - emit('UNFRIEND', { - curvePublic: curvePublic, - fromMe: true - }); - cb(); - }); - } - channel.wc.bcast(cryptMsg).then(function () {}, function (err) { - console.error(err); - }); - } catch (e) { - cb({error: e}); - } - }; - - var openChannel = function (data) { - var keys = data.keys; - var encryptor = data.encryptor || Curve.createEncryptor(keys); - var channel = { - id: data.channel, - isFriendChat: data.isFriendChat, - isPadChat: data.isPadChat, - padChan: data.padChan, - readOnly: data.readOnly, - sending: false, - messages: [], - userList: [], - mapId: {}, - }; - - channel.encrypt = function (msg) { - if (channel.readOnly) { return; } - return encryptor.encrypt(msg); - }; - channel.decrypt = data.decrypt || function (msg) { - return encryptor.decrypt(msg); - }; - - var onJoining = function (peer) { - if (peer === Msg.hk) { return; } - if (channel.userList.indexOf(peer) !== -1) { return; } - channel.userList.push(peer); - if (channel.readOnly) { return; } - - // Join event will be sent once we are able to ID this peer - var myData = createData(proxy); - delete myData.channel; - var msg = [Types.mapId, myData, channel.wc.myID]; - var msgStr = JSON.stringify(msg); - var cryptMsg = channel.encrypt(msgStr); - var data = { - channel: channel.id, - msg: cryptMsg - }; - network.sendto(peer, JSON.stringify(data)); - }; - - var onLeaving = function (peer) { - var i = channel.userList.indexOf(peer); - while (i !== -1) { - channel.userList.splice(i, 1); - i = channel.userList.indexOf(peer); - } - // update status - var otherData = channel.mapId[peer]; - if (!otherData) { return; } - - // Make sure the leaving user is not connected with another netflux id - if (channel.userList.some(function (nId) { - return channel.mapId[nId] - && channel.mapId[nId].curvePublic === otherData.curvePublic; - })) { return; } - - // Send the notification - emit('LEAVE', { - info: otherData, - id: channel.id - }); - }; - - var onOpen = function (chan) { - channel.wc = chan; - channels[data.channel] = channel; - - chan.on('message', function (msg, sender) { - onMessage(msg, sender, chan); - }); - - chan.members.forEach(function (peer) { - if (peer === Msg.hk) { return; } - if (channel.userList.indexOf(peer) !== -1) { return; } - channel.userList.push(peer); - }); - chan.on('join', onJoining); - chan.on('leave', onLeaving); - - // FIXME don't subscribe to the channel implicitly - getChannelMessagesSince(channel, data, keys); - }; - network.join(data.channel).then(onOpen, function (err) { - console.error(err); - }); - network.on('reconnect', function () { - if (channel && channel.stopped) { return; } - if (!channels[data.channel]) { return; } - - if (!joining[data.channel]) { - joining[data.channel] = function () { - console.log("reconnected to %s", data.channel); - }; - } else { - console.error("Reconnected to a chat channel (%s) which was not fully connected", data.channel); - } - - network.join(data.channel).then(onOpen, function (err) { - console.error(err); - }); - }); - }; - - messenger.getFriendList = function (cb) { - var friends = proxy.friends; - if (!friends) { return void cb(void 0, []); } - - cb(void 0, Object.keys(proxy.friends).filter(function (k) { - return k !== 'me'; - })); - }; - - var sendMessage = function (id, payload, cb) { - var channel = getChannel(id); - if (!channel) { return void cb({error: 'NO_CHANNEL'}); } - if (channel.readOnly) { return void cb({error: 'FORBIDDEN'}); } - if (!network.webChannels.some(function (wc) { - if (wc.id === channel.wc.id) { return true; } - })) { - return void cb({error: 'NO_SUCH_CHANNEL'}); - } - - var msg = [Types.message, proxy.curvePublic, +new Date(), payload]; - if (!channel.isFriendChat) { - var name = proxy[Constants.displayNameKey] || - Messages.anonymous + '#' + proxy.uid.slice(0,5); - msg.push(name); - } - var msgStr = JSON.stringify(msg); - var cryptMsg = channel.encrypt(msgStr); - - channel.wc.bcast(cryptMsg).then(function () { - pushMsg(channel, cryptMsg); - cb(); - }, function (err) { - cb({error: err}); - }); - }; - - var getStatus = function (chanId, cb) { - // Display green status if one member is not me - var channel = getChannel(chanId); - if (!channel) { return void cb('NO_SUCH_CHANNEL'); } - var online = channel.userList.some(function (nId) { - var data = channel.mapId[nId] || undefined; - if (!data) { return false; } - return data.curvePublic !== proxy.curvePublic; - }); - cb(online); - }; - - var getMyInfo = function (cb) { - cb({ - curvePublic: proxy.curvePublic, - displayName: proxy[Constants.displayNameKey] - }); - }; - - var loadFriend = function (friend, cb) { - var channel = friend.channel; - if (getChannel(channel)) { return void cb(); } - - joining[channel] = cb; - var keys = Curve.deriveKeys(friend.curvePublic, proxy.curvePrivate); - var data = { - keys: keys, - channel: friend.channel, - lastKnownHash: friend.lastKnownHash, - owners: [proxy.edPublic, friend.edPublic], - isFriendChat: true - }; - openChannel(data); - }; - - // Friend added in our contacts in the current worker - messenger.onFriendAdded = function (friendData) { - if (!allowFriendsChannels) { return; } - var friend = friends[friendData.curvePublic]; - if (typeof(friend) !== 'object') { return; } - var channel = friend.channel; - if (!channel) { return; } - loadFriend(friend, function () { - emit('FRIEND', { - curvePublic: friend.curvePublic, - }); - }); - }; - messenger.onFriendRemoved = function (curvePublic, chanId) { - var channel = channels[chanId]; - if (!channel) { return; } - if (channel.wc) { - channel.wc.leave(Types.unfriend); - } - delete channels[channel.id]; - emit('UNFRIEND', { - curvePublic: curvePublic, - fromMe: true - }); - }; - - var ready = false; - var initialized = false; - var init = function () { - allowFriendsChannels = true; - if (initialized) { return; } - initialized = true; - var friends = getFriendList(proxy); - - nThen(function (waitFor) { - Object.keys(friends).forEach(function (key) { - if (key === 'me') { return; } - var friend = clone(friends[key]); - if (typeof(friend) !== 'object') { return; } - var channel = friend.channel; - if (!channel) { return; } - loadFriend(friend, waitFor()); - }); - // TODO load rooms - }).nThen(function () { - ready = true; - emit('READY'); - }); - }; - //init(); - - var getRooms = function (data, cb) { - if (data && data.curvePublic) { - var curvePublic = data.curvePublic; - // We need to get data about a new friend's room - var friend = getFriend(proxy, curvePublic); - if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); } - var channel = getChannel(friend.channel); - if (!channel) { return void cb({error: 'NO_SUCH_CHANNEL'}); } - return void cb([{ - id: channel.id, - isFriendChat: true, - name: friend.displayName, - lastKnownHash: friend.lastKnownHash, - curvePublic: friend.curvePublic, - messages: channel.messages - }]); - } - - if (data && data.padChat) { - var pCChannel = getChannel(data.padChat); - if (!pCChannel) { return void cb({error: 'NO_SUCH_CHANNEL'}); } - return void cb([{ - id: pCChannel.id, - isPadChat: true, - messages: pCChannel.messages - }]); - } - - var rooms = Object.keys(channels).map(function (id) { - var r = getChannel(id); - var name, lastKnownHash, curvePublic; - if (r.isFriendChat) { - var friend = getFriendFromChannel(id); - if (!friend) { return null; } - name = friend.displayName; - lastKnownHash = friend.lastKnownHash; - curvePublic = friend.curvePublic; - } else if (r.isPadChat) { - return; - } else { - // TODO room get metadata (name) && lastKnownHash - } - return { - id: r.id, - isFriendChat: r.isFriendChat, - name: name, - lastKnownHash: lastKnownHash, - curvePublic: curvePublic, - messages: r.messages - }; - }).filter(function (x) { return x; }); - cb(rooms); - }; - - var getUserList = function (data, cb) { - var room = getChannel(data.id); - if (!room) { return void cb({error: 'NO_SUCH_CHANNEL'}); } - if (room.isFriendChat) { - var friend = getFriendFromChannel(data.id); - if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); } - cb([friend]); - } else { - // TODO room userlist in rooms... - // (this is the static userlist, not the netflux one) - cb([]); - } - }; - - var validateKeys = {}; - messenger.storeValidateKey = function (chan, key) { - validateKeys[chan] = key; - }; - - var openPadChat = function (data, cb) { - var channel = data.channel; - if (getChannel(channel)) { - emit('PADCHAT_READY', channel); - return void cb(); - } - var secret = data.secret; - if (secret.keys.cryptKey) { - secret.keys.cryptKey = convertToUint8(secret.keys.cryptKey); - } - var encryptor = Crypto.createEncryptor(secret.keys); - var vKey = (secret.keys && secret.keys.validateKey) || validateKeys[secret.channel]; - var chanData = { - padChan: data.secret && data.secret.channel, - readOnly: typeof(secret.keys) === "object" && !secret.keys.validateKey, - encryptor: encryptor, - channel: data.channel, - isPadChat: true, - decrypt: function (msg) { - return encryptor.decrypt(msg, vKey); - }, - //lastKnownHash: friend.lastKnownHash, - //owners: [proxy.edPublic, friend.edPublic], - //isFriendChat: true - }; - openChannel(chanData); - joining[channel] = function () { - emit('PADCHAT_READY', channel); - }; - cb(); - }; - - var clearOwnedChannel = function (id, cb) { - var channel = getChannel(id); - if (!channel) { return void cb({error: 'NO_CHANNEL'}); } - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - store.rpc.clearOwnedChannel(id, function (err) { - cb({error:err}); - }); - channel.messages = []; - }; - - network.on('disconnect', function () { - emit('DISCONNECT'); - }); - network.on('reconnect', function () { - emit('RECONNECT'); - }); - - messenger.leavePad = function (padChan) { - // Leave chat and prevent reconnect when we leave a pad - delete validateKeys[padChan]; - Object.keys(channels).some(function (chatChan) { - var channel = channels[chatChan]; - if (channel.padChan !== padChan) { return; } - if (channel.wc) { channel.wc.leave(); } - channel.stopped = true; - delete channels[chatChan]; - return true; - }); - }; - - messenger.execCommand = function (obj, cb) { - var cmd = obj.cmd; - var data = obj.data; - if (cmd === 'INIT_FRIENDS') { - init(); - return void cb(); - } - if (cmd === 'IS_READY') { - return void cb(ready); - } - if (cmd === 'GET_ROOMS') { - return void getRooms(data, cb); - } - if (cmd === 'GET_USERLIST') { - return void getUserList(data, cb); - } - if (cmd === 'OPEN_PAD_CHAT') { - return void openPadChat(data, cb); - } - if (cmd === 'GET_MY_INFO') { - return void getMyInfo(cb); - } - if (cmd === 'REMOVE_FRIEND') { - return void removeFriend(data, cb); - } - if (cmd === 'GET_STATUS') { - return void getStatus(data, cb); - } - if (cmd === 'GET_MORE_HISTORY') { - return void getMoreHistory(data.id, data.sig, data.count, cb); - } - if (cmd === 'SEND_MESSAGE') { - return void sendMessage(data.id, data.content, cb); - } - if (cmd === 'SET_CHANNEL_HEAD') { - return void setChannelHead(data.id, data.sig, cb); - } - if (cmd === 'CLEAR_OWNED_CHANNEL') { - return void clearOwnedChannel(data, cb); - } - }; - - Object.freeze(messenger); - - return messenger; - }; - - return Msg; -}); diff --git a/www/common/common-notifier.js b/www/common/common-notifier.js index 99415b553..688c677a6 100644 --- a/www/common/common-notifier.js +++ b/www/common/common-notifier.js @@ -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 + ')'; } diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 129b6a46f..b3e03bb7d 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -104,11 +104,13 @@ define([ var channel = data.channel; var owners = data.owners || []; var pending_owners = data.pending_owners || []; + var teams = priv.teams; + var teamOwner = data.teamId; var redrawAll = function () {}; - var div1 = h('div.cp-share-friends.cp-share-column.cp-ownership'); - var div2 = h('div.cp-share-friends.cp-share-column.cp-ownership'); + var div1 = h('div.cp-usergrid-user.cp-share-column.cp-ownership'); + var div2 = h('div.cp-usergrid-user.cp-share-column.cp-ownership'); var $div1 = $(div1); var $div2 = $(div2); @@ -124,24 +126,26 @@ define([ return true; } }); + Object.keys(teams).some(function (id) { + if (teams[id].edPublic === ed) { + f = teams[id]; + f.teamId = id; + } + }); if (ed === edPublic) { f = f || user; - if (f.name) { - f.displayName = f.name; - f.edPublic = edPublic; - } + if (f.name) { f.edPublic = edPublic; } } _owners[ed] = f || { displayName: Messages._getKey('owner_unknownUser', [ed]), - notifications: true, edPublic: ed, }; }); var msg = pending ? Messages.owner_removePendingText : Messages.owner_removeText; - var removeCol = UIElements.getFriendsList(msg, { + var removeCol = UIElements.getUserGrid(msg, { common: common, - friends: _owners, + data: _owners, noFilter: true }, function () { }); @@ -152,14 +156,15 @@ define([ var removeButton = h('button.no-margin', btnMsg); $(removeButton).click(function () { // Check selection - var $sel = $div.find('.cp-share-friend.cp-selected'); + var $sel = $div.find('.cp-usergrid-user.cp-selected'); var sel = $sel.toArray(); if (!sel.length) { return; } var me = false; var toRemove = sel.map(function (el) { var ed = $(el).attr('data-ed'); if (!ed) { return; } - if (ed === edPublic) { me = true; } + if (teamOwner && teams[teamOwner] && teams[teamOwner].edPublic === ed) { me = true; } + if (ed === edPublic && !teamOwner) { me = true; } return ed; }).filter(function (x) { return x; }); NThen(function (waitFor) { @@ -175,7 +180,8 @@ define([ sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS', - value: toRemove + value: toRemove, + teamId: teamOwner }, waitFor(function (err, res) { err = err || (res && res.error); if (err) { @@ -189,7 +195,8 @@ define([ })); }).nThen(function (waitFor) { sel.forEach(function (el) { - var friend = friends[$(el).attr('data-curve')]; + var curve = $(el).attr('data-curve'); + var friend = curve === user.curvePublic ? user : friends[curve]; if (!friend) { return; } common.mailbox.sendTo("RM_OWNER", { channel: channel, @@ -218,29 +225,60 @@ define([ // Add owners column var drawAdd = function () { + var $div = $(h('div.cp-share-column')); var _friends = JSON.parse(JSON.stringify(friends)); Object.keys(_friends).forEach(function (curve) { if (owners.indexOf(_friends[curve].edPublic) !== -1 || - pending_owners.indexOf(_friends[curve].edPublic) !== -1) { + pending_owners.indexOf(_friends[curve].edPublic) !== -1 || + !_friends[curve].notifications) { delete _friends[curve]; } }); - var addCol = UIElements.getFriendsList(Messages.owner_addText, { + var addCol = UIElements.getUserGrid(Messages.owner_addText, { common: common, - friends: _friends + data: _friends }, function () { //console.log(arguments); }); - $div2 = $(addCol.div); + $div.append(addCol.div); + + var teamsData = Util.tryParse(JSON.stringify(priv.teams)) || {}; + Object.keys(teamsData).forEach(function (id) { + var t = teamsData[id]; + t.teamId = id; + if (owners.indexOf(t.edPublic) !== -1 || pending_owners.indexOf(t.edPublic) !== -1) { + delete teamsData[id]; + } + }); + var teamsList = UIElements.getUserGrid(Messages.owner_addTeamText, { + common: common, + noFilter: true, + data: teamsData + }, function () {}); + $div.append(teamsList.div); + // When clicking on the add button, we get the selected users. var addButton = h('button.no-margin', Messages.owner_addButton); $(addButton).click(function () { // Check selection - var $sel = $div2.find('.cp-share-friend.cp-selected'); + var $sel = $div.find('.cp-usergrid-user.cp-selected'); var sel = $sel.toArray(); if (!sel.length) { return; } var toAdd = sel.map(function (el) { - return friends[$(el).attr('data-curve')].edPublic; + var curve = $(el).attr('data-curve'); + // If the pad is woned by a team, we can transfer ownership to ourselves + if (curve === user.curvePublic && teamOwner) { return priv.edPublic; } + var friend = friends[curve]; + if (!friend) { return; } + return friend.edPublic; + }).filter(function (x) { return x; }); + var toAddTeams = sel.map(function (el) { + var team = teamsData[$(el).attr('data-teamid')]; + if (!team || !team.edPublic) { return; } + return { + edPublic: team.edPublic, + id: $(el).attr('data-teamid') + }; }).filter(function (x) { return x; }); NThen(function (waitFor) { @@ -252,24 +290,62 @@ define([ } })); }).nThen(function (waitFor) { - // Send the command - sframeChan.query('Q_SET_PAD_METADATA', { - channel: channel, - command: 'ADD_PENDING_OWNERS', - value: toAdd - }, waitFor(function (err, res) { - err = err || (res && res.error); - if (err) { - waitFor.abort(); - redrawAll(); - var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden - : Messages.error; - return void UI.warn(text); - } - })); + // Add one of our teams as an owner + if (toAddTeams.length) { + // Send the command + sframeChan.query('Q_SET_PAD_METADATA', { + channel: channel, + command: 'ADD_OWNERS', + value: toAddTeams.map(function (obj) { return obj.edPublic; }), + teamId: teamOwner + }, waitFor(function (err, res) { + err = err || (res && res.error); + if (err) { + waitFor.abort(); + redrawAll(); + var text = err === "INSUFFICIENT_PERMISSIONS" ? + Messages.fm_forbidden : Messages.error; + return void UI.warn(text); + } + var isTemplate = priv.isTemplate || data.isTemplate; + toAddTeams.forEach(function (obj) { + sframeChan.query('Q_STORE_IN_TEAM', { + href: data.href || data.rohref, + password: data.password, + path: isTemplate ? ['template'] : undefined, + title: data.title || '', + teamId: obj.id + }, waitFor(function (err) { + if (err) { return void console.error(err); } + console.warn(obj.id); + })); + }); + })); + } + }).nThen(function (waitFor) { + // Offer ownership to a friend + if (toAdd.length) { + // Send the command + sframeChan.query('Q_SET_PAD_METADATA', { + channel: channel, + command: 'ADD_PENDING_OWNERS', + value: toAdd, + teamId: teamOwner + }, waitFor(function (err, res) { + err = err || (res && res.error); + if (err) { + waitFor.abort(); + redrawAll(); + var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden + : Messages.error; + return void UI.warn(text); + } + })); + } }).nThen(function (waitFor) { sel.forEach(function (el) { - var friend = friends[$(el).attr('data-curve')]; + var curve = $(el).attr('data-curve'); + var friend = curve === user.curvePublic ? user : friends[curve]; if (!friend) { return; } common.mailbox.sendTo("ADD_OWNER", { channel: channel, @@ -294,8 +370,8 @@ define([ UI.log(Messages.saved); }); }); - $div2.append(h('p', addButton)); - return $div2; + $div.append(h('p', addButton)); + return $div; }; redrawAll = function (md) { @@ -348,49 +424,95 @@ define([ var draw = function () { var $d = $('
'); - $('