Merge branch 'soon' of github.com:xwiki-labs/cryptpad into soon
This commit is contained in:
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,4 +1,51 @@
|
|||||||
# HimalayanQuail (3.7.0)
|
# IsolobodonPortoricensis release (3.8.0)
|
||||||
|
|
||||||
|
We had some trouble finding an extinct animal whose name started with "I", and we had to resort to using a scientific name.
|
||||||
|
Despite this long name, this was a very short release cycle.
|
||||||
|
It's the last release of 2019, so we hope you like it!
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
During this release cycle we prioritized the mitigation of some social abuse vectors and the ability to invite users to a team via a link.
|
||||||
|
We have more improvements planned for both features, but we wanted to release what we had before the end of the year as our team is taking a little time off to recharge for 2020.
|
||||||
|
|
||||||
|
## Update notes
|
||||||
|
|
||||||
|
This is a small and simple release. We made a very minor improvement to the server which will require a restart, but everything will still work if you choose not to.
|
||||||
|
|
||||||
|
Update from 3.7.0 to 3.8.0 with the following procedure:
|
||||||
|
|
||||||
|
1. Take your server down
|
||||||
|
2. Get the latest code with `git pull origin master`
|
||||||
|
3. Bring your server back up
|
||||||
|
|
||||||
|
Or if you've set up your admin interface:
|
||||||
|
|
||||||
|
1. Pull the latest code
|
||||||
|
2. Click the admin panel's "Flush cache" button
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* We updated a bunch of styles to improve the platform's visual consistency:
|
||||||
|
* prettier buttons
|
||||||
|
* elimination of rounded corners on buttons, text inputs, and password inputs
|
||||||
|
* We've fixed the default styles on embedded media while their content is loading
|
||||||
|
* The button to add a user as a contact on their profile page now has a more prominent position at the top of the page
|
||||||
|
* Users also have the option of muting other people via their profile page.
|
||||||
|
* these users will not know that you've muted them.
|
||||||
|
* you can review the complete list of all the people you've muted on your contacts page
|
||||||
|
* you can mute or unmute from the contacts page as well as their profile
|
||||||
|
* changes to a user's mute status propagate across pages in real-time
|
||||||
|
* Some of our Finnish-speaking users have become contributors via our weblate instance (https://weblate.cryptpad.fr/)
|
||||||
|
* we're always looking for more translators to help more people protect their data, so don't hesitate to contact us if you want to help
|
||||||
|
* Finally, it's now possible to invite users to a team by creating and sharing a personalized one-time-use link.
|
||||||
|
* team owners and admins can try it out via their teams' "Members" tab
|
||||||
|
|
||||||
|
## Bug fixes
|
||||||
|
|
||||||
|
* We've fixed a few subtle bugs where various contact status and our one-to-one chat functionality could get into a bad state.
|
||||||
|
|
||||||
|
# HimalayanQuail release (3.7.0)
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ define([
|
|||||||
])*/
|
])*/
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
h('div.cp-version-footer', "CryptPad v3.7.0 (HimalayanQuail)")
|
h('div.cp-version-footer', "CryptPad v3.8.0 (IsolobodonPortoricensis)")
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@import (reference) "./variables.less";
|
@import (reference) "./variables.less";
|
||||||
@import (reference) "./avatar.less";
|
@import (reference) "./avatar.less";
|
||||||
@import (reference) "./tools.less";
|
@import (reference) "./tools.less";
|
||||||
|
@import (reference) "./buttons.less";
|
||||||
|
|
||||||
.alertify_main() {
|
.alertify_main() {
|
||||||
--LessLoader_require: LessLoader_currentFile();
|
--LessLoader_require: LessLoader_currentFile();
|
||||||
@@ -71,6 +72,10 @@
|
|||||||
z-index: 100000; // alertify container
|
z-index: 100000; // alertify container
|
||||||
font: @colortheme_app-font;
|
font: @colortheme_app-font;
|
||||||
|
|
||||||
|
.cp-inline-alert-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&.forefront {
|
&.forefront {
|
||||||
z-index: @max-z-index; // alertify max forefront
|
z-index: @max-z-index; // alertify max forefront
|
||||||
}
|
}
|
||||||
@@ -221,28 +226,6 @@
|
|||||||
::-ms-input-placeholder { /* Microsoft Edge */
|
::-ms-input-placeholder { /* Microsoft Edge */
|
||||||
color: @cryptpad_color_grey;
|
color: @cryptpad_color_grey;
|
||||||
}
|
}
|
||||||
input:not(.form-control), textarea {
|
|
||||||
background-color: @alertify-input-fg;
|
|
||||||
color: @cryptpad_text_col;
|
|
||||||
border: 1px solid @alertify-input-bg;
|
|
||||||
margin-bottom: @alertify_padding-base;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 100%;
|
|
||||||
padding: @alertify_padding-base;
|
|
||||||
&[readonly] {
|
|
||||||
background-color: @alertify-light-bg;
|
|
||||||
color: @cryptpad_text_col;
|
|
||||||
border-color: @alertify-light-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 8px;
|
|
||||||
&[readonly] {
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span.cp-password-container {
|
span.cp-password-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -257,6 +240,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fa-question-circle { // help links to FAQ
|
||||||
|
color: @colortheme_logo-2;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
input[type="checkbox"], input[type="radio"] {
|
input[type="checkbox"], input[type="radio"] {
|
||||||
width: auto;
|
width: auto;
|
||||||
@@ -271,99 +260,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button) {
|
.buttons_main();
|
||||||
|
input:not(.form-control), textarea {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
background-color: @colortheme_alertify-cancel;
|
button {
|
||||||
box-sizing: border-box;
|
|
||||||
position: relative;
|
|
||||||
outline: 0;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
align-items: center;
|
position: relative;
|
||||||
padding: 0 6px;
|
|
||||||
margin: 6px 8px;
|
margin: 6px 8px;
|
||||||
line-height: 36px;
|
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
white-space: nowrap;
|
|
||||||
min-width: 88px;
|
min-width: 88px;
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 14px;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
color: @alertify-btn-fg;
|
|
||||||
border: 1px solid @alertify-btn-fg;
|
|
||||||
|
|
||||||
&.no-margin {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover, &:active {
|
|
||||||
background-color: @alertify-light-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.safe, &.danger {
|
|
||||||
color: @colortheme_old-base;
|
|
||||||
white-space: normal;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
&.danger {
|
|
||||||
background-color: @colortheme_alertify-red;
|
|
||||||
border-color: @colortheme_alertify-red-border;
|
|
||||||
color: @colortheme_alertify-red-color;
|
|
||||||
&:hover, &:active {
|
|
||||||
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.safe {
|
|
||||||
background-color: @colortheme_alertify-green;
|
|
||||||
border-color: @colortheme_alertify-green-border;
|
|
||||||
color: @colortheme_alertify-green-color;
|
|
||||||
&:hover, &:active {
|
|
||||||
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-green, 10%), lighten(@colortheme_alertify-green, 10%));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
background-color: @colortheme_alertify-primary;
|
|
||||||
color: @colortheme_alertify-primary-text;
|
|
||||||
border-color: @colortheme_alertify-primary-border;
|
|
||||||
font-weight: bold;
|
|
||||||
&:hover, &:active {
|
|
||||||
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-primary, 10%), lighten(@colortheme_alertify-primary, 10%));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.cancel {
|
|
||||||
border-color: @colortheme_alertify-cancel-border;
|
|
||||||
color: @colortheme_alertify-cancel-border;
|
|
||||||
&:hover, &:hover {
|
|
||||||
background-color: fade(@colortheme_alertify-cancel-border, 25%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
//border: 1px dotted @alertify-base;
|
|
||||||
box-shadow: 0px 0px 5px @colortheme_alertify-primary;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
&::-moz-focus-inner {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed !important;
|
|
||||||
background-color: @colortheme_alertify-disabled;
|
|
||||||
color: @colortheme_alertify-disabled-text;
|
|
||||||
border-color: @colortheme_alertify-disabled-border;
|
|
||||||
&:hover, &:active {
|
|
||||||
background-color: @colortheme_alertify-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
@@ -371,8 +278,8 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
button {
|
button {
|
||||||
margin: 0px !important;
|
margin: 0px !important;
|
||||||
&:not(:last-child) {
|
&:not(:first-child) {
|
||||||
margin-right: @alertify_padding-base !important;
|
margin-left: @alertify_padding-base !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
customize.dist/src/less2/include/buttons.less
Normal file
129
customize.dist/src/less2/include/buttons.less
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
@import (reference) "./colortheme-all.less";
|
||||||
|
@import (reference) "./variables.less";
|
||||||
|
|
||||||
|
.buttons_main() {
|
||||||
|
@alertify-fore: @colortheme_modal-fg;
|
||||||
|
@alertify-btn-fg: @alertify-fore;
|
||||||
|
@alertify-light-bg: fade(@alertify-fore, 25%);
|
||||||
|
@alertify_padding-base: @variables_padding;
|
||||||
|
@alertify-input-bg: @colortheme_modal-input;
|
||||||
|
@alertify-input-fg: @colortheme_modal-input-fg;
|
||||||
|
|
||||||
|
input:not(.form-control), textarea {
|
||||||
|
background-color: @alertify-input-fg;
|
||||||
|
color: @cryptpad_text_col;
|
||||||
|
border: 1px solid @alertify-input-bg;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 100%;
|
||||||
|
padding: @alertify_padding-base;
|
||||||
|
&[readonly] {
|
||||||
|
background-color: @alertify-light-bg;
|
||||||
|
color: @cryptpad_text_col;
|
||||||
|
border-color: @alertify-input-fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px;
|
||||||
|
&[readonly] {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
button:not(.pure-button):not(.md-button):not(.mdl-button) {
|
||||||
|
|
||||||
|
background-color: @colortheme_alertify-cancel;
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: 0;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 6px;
|
||||||
|
line-height: 36px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
margin-right: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
color: @alertify-btn-fg;
|
||||||
|
border: 1px solid @alertify-btn-fg;
|
||||||
|
|
||||||
|
&.no-margin {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
background-color: lighten(@alertify-fore, 35%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.safe, &.danger, &.btn-safe, &.btn-danger {
|
||||||
|
color: @colortheme_old-base;
|
||||||
|
white-space: normal;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
&.danger, &.btn-danger {
|
||||||
|
background-color: @colortheme_alertify-red;
|
||||||
|
border-color: @colortheme_alertify-red-border;
|
||||||
|
color: @colortheme_alertify-red-color;
|
||||||
|
&:hover, &:active {
|
||||||
|
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.safe, &.btn-safe {
|
||||||
|
background-color: @colortheme_alertify-green;
|
||||||
|
border-color: @colortheme_alertify-green-border;
|
||||||
|
color: @colortheme_alertify-green-color;
|
||||||
|
&:hover, &:active {
|
||||||
|
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-green, 10%), lighten(@colortheme_alertify-green, 10%));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary, &.btn-primary {
|
||||||
|
background-color: @colortheme_alertify-primary;
|
||||||
|
color: @colortheme_alertify-primary-text;
|
||||||
|
border-color: @colortheme_alertify-primary-border;
|
||||||
|
font-weight: bold;
|
||||||
|
&:hover, &:active {
|
||||||
|
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-primary, 10%), lighten(@colortheme_alertify-primary, 10%));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cancel, &.btn-cancel {
|
||||||
|
border-color: @colortheme_alertify-cancel-border;
|
||||||
|
color: @colortheme_alertify-cancel-border;
|
||||||
|
&:hover, &:hover {
|
||||||
|
background-color: fade(@colortheme_alertify-cancel-border, 25%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
//border: 1px dotted @alertify-base;
|
||||||
|
box-shadow: 0px 0px 5px @colortheme_alertify-primary;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
&::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
background-color: @colortheme_alertify-disabled;
|
||||||
|
color: @colortheme_alertify-disabled-text;
|
||||||
|
border-color: @colortheme_alertify-disabled-border;
|
||||||
|
&:hover, &:active {
|
||||||
|
background-color: @colortheme_alertify-disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,6 +7,13 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
media-tag:empty {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid #BBB;
|
||||||
|
}
|
||||||
|
|
||||||
media-tag img {
|
media-tag img {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 100% !important;
|
max-height: 100% !important;
|
||||||
|
|||||||
@@ -78,6 +78,9 @@
|
|||||||
.cp-app-contacts-name {
|
.cp-app-contacts-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.cp-app-contacts-icons {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(0,0,0,0.3);
|
background-color: rgba(0,0,0,0.3);
|
||||||
@@ -89,6 +92,7 @@
|
|||||||
.cp-app-contacts-remove {
|
.cp-app-contacts-remove {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: darken(@color, 20%);
|
color: darken(@color, 20%);
|
||||||
}
|
}
|
||||||
@@ -121,8 +125,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.cp-app-contacts-muted-button {
|
||||||
|
margin: 10px;
|
||||||
|
border: 0;
|
||||||
|
display: none;
|
||||||
|
order: 3;
|
||||||
|
.fa-bell-slash {
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-contacts-muted-table {
|
||||||
|
.cp-contacts-muted-user {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
.cp-avatar {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#cp-app-contacts-container.cp-app-contacts-inapp {
|
#cp-app-contacts-container.cp-app-contacts-inapp {
|
||||||
#cp-app-contacts-friendlist {
|
#cp-app-contacts-friendlist {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -28,4 +28,26 @@
|
|||||||
.cp-app-prop-content {
|
.cp-app-prop-content {
|
||||||
color: @cryptpad_text_col;
|
color: @cryptpad_text_col;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// teams invite modal
|
||||||
|
.cp-teams-invite-block {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.cp-teams-invite-message {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
.cp-teams-invite-alert {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.cp-teams-invite-spinner {
|
||||||
|
font-size: 1.2em;
|
||||||
|
.fa {
|
||||||
|
margin-right: 10px;;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cp-teams-help {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import (reference) "/customize/src/less2/include/colortheme-all.less";
|
@import (reference) "/customize/src/less2/include/colortheme-all.less";
|
||||||
@import (reference) "/customize/src/less2/include/leftside-menu.less";
|
@import (reference) "/customize/src/less2/include/leftside-menu.less";
|
||||||
|
@import (reference) "/customize/src/less2/include/buttons.less";
|
||||||
|
|
||||||
@sidebar_button-width: 400px;
|
@sidebar_button-width: 400px;
|
||||||
|
|
||||||
@@ -95,9 +96,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
.buttons_main();
|
||||||
}
|
}
|
||||||
[type="text"], [type="password"], button {
|
[type="text"], [type="password"], button {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
min-width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -106,12 +109,12 @@
|
|||||||
width: @sidebar_button-width;
|
width: @sidebar_button-width;
|
||||||
input {
|
input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-radius: 0.25em 0 0 0.25em;
|
//border-radius: 0.25em 0 0 0.25em;
|
||||||
border: 1px solid #adadad;
|
border: 1px solid #adadad;
|
||||||
border-right: 0px;
|
border-right: 0px;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
border-radius: 0 0.25em 0.25em 0;
|
//border-radius: 0 0.25em 0.25em 0;
|
||||||
//border: 1px solid #adadad;
|
//border: 1px solid #adadad;
|
||||||
border-left: 0px;
|
border-left: 0px;
|
||||||
}
|
}
|
||||||
@@ -119,6 +122,13 @@
|
|||||||
&>div {
|
&>div {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
button.btn {
|
||||||
|
margin: 0 5px 0 0;
|
||||||
|
}
|
||||||
|
span.cp-password-container {
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
/*
|
||||||
button.btn {
|
button.btn {
|
||||||
@button-bg: @colortheme_sidebar-button-bg;
|
@button-bg: @colortheme_sidebar-button-bg;
|
||||||
@button-red-bg: @colortheme_sidebar-button-red-bg;
|
@button-red-bg: @colortheme_sidebar-button-red-bg;
|
||||||
@@ -126,6 +136,9 @@
|
|||||||
background-color: @button-bg;
|
background-color: @button-bg;
|
||||||
border-color: darken(@button-bg, 10%);
|
border-color: darken(@button-bg, 10%);
|
||||||
color: white;
|
color: white;
|
||||||
|
.fa {
|
||||||
|
margin-right: 0.2em;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: darken(@button-bg, 10%);
|
background-color: darken(@button-bg, 10%);
|
||||||
}
|
}
|
||||||
@@ -146,6 +159,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cryptpad",
|
"name": "cryptpad",
|
||||||
"version": "3.7.0",
|
"version": "3.8.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "cryptpad",
|
"name": "cryptpad",
|
||||||
"description": "realtime collaborative visual editor with zero knowlege server",
|
"description": "realtime collaborative visual editor with zero knowlege server",
|
||||||
"version": "3.7.0",
|
"version": "3.8.0",
|
||||||
"license": "AGPL-3.0+",
|
"license": "AGPL-3.0+",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ app.get('/api/config', function(req, res){
|
|||||||
// FIXME don't send websocketURL if websocketPath is provided. deprecated.
|
// FIXME don't send websocketURL if websocketPath is provided. deprecated.
|
||||||
websocketURL:'ws' + ((useSecureWebsockets) ? 's' : '') + '://' + host + ':' +
|
websocketURL:'ws' + ((useSecureWebsockets) ? 's' : '') + '://' + host + ':' +
|
||||||
websocketPort + '/cryptpad_websocket',
|
websocketPort + '/cryptpad_websocket',
|
||||||
httpUnsafeOrigin: config.httpUnsafeOrigin,
|
httpUnsafeOrigin: config.httpUnsafeOrigin.replace(/^\s*/, ''),
|
||||||
adminEmail: config.adminEmail,
|
adminEmail: config.adminEmail,
|
||||||
adminKeys: admins,
|
adminKeys: admins,
|
||||||
inactiveTime: config.inactiveTime,
|
inactiveTime: config.inactiveTime,
|
||||||
|
|||||||
@@ -98,6 +98,13 @@
|
|||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
media-tag:empty {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid #BBB;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown_main();
|
.markdown_main();
|
||||||
.cp-app-code-preview-empty {
|
.cp-app-code-preview-empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ define(function() {
|
|||||||
* users and these users will be redirected to the login page if they still try to access
|
* users and these users will be redirected to the login page if they still try to access
|
||||||
* the app
|
* the app
|
||||||
*/
|
*/
|
||||||
config.registeredOnlyTypes = ['teams', 'file', 'contacts', 'oodoc', 'ooslide', 'sheet', 'notifications'];
|
config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'sheet', 'notifications'];
|
||||||
|
|
||||||
/* CryptPad is available is multiple languages, but only English and French are maintained
|
/* 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
|
* by the developers. The other languages may be outdated, and any missing string for a langauge
|
||||||
|
|||||||
@@ -231,10 +231,14 @@ Version 1
|
|||||||
}
|
}
|
||||||
if (['invite'].indexOf(type) !== -1) {
|
if (['invite'].indexOf(type) !== -1) {
|
||||||
parsed.type = 'invite';
|
parsed.type = 'invite';
|
||||||
if (hashArr[1] && hashArr[1] === '1') {
|
if (hashArr[1] && hashArr[1] === '2') {
|
||||||
parsed.version = 1;
|
parsed.version = 2;
|
||||||
parsed.channel = hashArr[2];
|
parsed.app = hashArr[2];
|
||||||
parsed.pubkey = hashArr[3].replace(/-/g, '/');
|
parsed.mode = hashArr[3];
|
||||||
|
parsed.key = hashArr[4];
|
||||||
|
|
||||||
|
options = hashArr.slice(5);
|
||||||
|
parsed.password = options.indexOf('p') !== -1;
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ define([
|
|||||||
opt = opt || {};
|
opt = opt || {};
|
||||||
|
|
||||||
var inputBlock = opt.password ? UI.passwordInput() : dialog.textInput();
|
var inputBlock = opt.password ? UI.passwordInput() : dialog.textInput();
|
||||||
var input = opt.password ? $(inputBlock).find('input')[0] : inputBlock;
|
var input = $(inputBlock).is('input') ? inputBlock : $(inputBlock).find('input')[0];
|
||||||
input.value = typeof(def) === 'string'? def: '';
|
input.value = typeof(def) === 'string'? def: '';
|
||||||
|
|
||||||
var message;
|
var message;
|
||||||
@@ -592,6 +592,7 @@ define([
|
|||||||
}];
|
}];
|
||||||
var modal = dialog.customModal(content, {buttons: buttons});
|
var modal = dialog.customModal(content, {buttons: buttons});
|
||||||
UI.openCustomModal(modal);
|
UI.openCustomModal(modal);
|
||||||
|
return modal;
|
||||||
};
|
};
|
||||||
|
|
||||||
UI.log = function (msg) {
|
UI.log = function (msg) {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ define([
|
|||||||
var myData = createData(store.proxy, false);
|
var myData = createData(store.proxy, false);
|
||||||
if (store.proxy.friends) {
|
if (store.proxy.friends) {
|
||||||
store.proxy.friends.me = myData;
|
store.proxy.friends.me = myData;
|
||||||
|
delete store.proxy.friends.me.channel;
|
||||||
}
|
}
|
||||||
if (store.modules['team']) {
|
if (store.modules['team']) {
|
||||||
store.modules['team'].updateMyData(myData);
|
store.modules['team'].updateMyData(myData);
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ define([
|
|||||||
'/customize/application_config.js',
|
'/customize/application_config.js',
|
||||||
'/customize/pages.js',
|
'/customize/pages.js',
|
||||||
'/bower_components/nthen/index.js',
|
'/bower_components/nthen/index.js',
|
||||||
|
'/common/invitation.js',
|
||||||
|
|
||||||
'css!/customize/fonts/cptools/style.css',
|
'css!/customize/fonts/cptools/style.css',
|
||||||
'/bower_components/croppie/croppie.min.js',
|
'/bower_components/croppie/croppie.min.js',
|
||||||
'css!/bower_components/croppie/croppie.css',
|
'css!/bower_components/croppie/croppie.css',
|
||||||
], function ($, Config, Util, Hash, Language, UI, Constants, Feedback, h, MediaTag, Clipboard,
|
], function ($, Config, Util, Hash, Language, UI, Constants, Feedback, h, MediaTag, Clipboard,
|
||||||
Messages, AppConfig, Pages, NThen) {
|
Messages, AppConfig, Pages, NThen, InviteInner) {
|
||||||
var UIElements = {};
|
var UIElements = {};
|
||||||
|
|
||||||
// Configure MediaTags to use our local viewer
|
// Configure MediaTags to use our local viewer
|
||||||
@@ -1557,8 +1559,11 @@ define([
|
|||||||
var team = privateData.teams[config.teamId];
|
var team = privateData.teams[config.teamId];
|
||||||
if (!team) { return void UI.warn(Messages.error); }
|
if (!team) { return void UI.warn(Messages.error); }
|
||||||
|
|
||||||
|
var origin = privateData.origin;
|
||||||
|
|
||||||
var module = config.module || common.makeUniversal('team');
|
var module = config.module || common.makeUniversal('team');
|
||||||
|
|
||||||
|
// Invite contacts
|
||||||
var $div;
|
var $div;
|
||||||
var refreshButton = function () {
|
var refreshButton = function () {
|
||||||
if (!$div) { return; }
|
if (!$div) { return; }
|
||||||
@@ -1572,20 +1577,16 @@ define([
|
|||||||
$btn.prop('disabled', 'disabled');
|
$btn.prop('disabled', 'disabled');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var getContacts = function () {
|
||||||
var list = UIElements.getUserGrid(Messages.team_pickFriends, {
|
var list = UIElements.getUserGrid(Messages.team_pickFriends, {
|
||||||
common: common,
|
common: common,
|
||||||
data: config.friends,
|
data: config.friends,
|
||||||
large: true
|
large: true
|
||||||
}, refreshButton);
|
}, refreshButton);
|
||||||
$div = $(list.div);
|
var div = h('div.contains-nav');
|
||||||
refreshButton();
|
var $div = $(div);
|
||||||
|
$div.append(list.div);
|
||||||
var buttons = [{
|
var contactsButtons = [{
|
||||||
className: 'cancel',
|
|
||||||
name: Messages.cancel,
|
|
||||||
onClick: function () {},
|
|
||||||
keys: [27]
|
|
||||||
}, {
|
|
||||||
className: 'primary',
|
className: 'primary',
|
||||||
name: Messages.team_inviteModalButton,
|
name: Messages.team_inviteModalButton,
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
@@ -1609,11 +1610,193 @@ define([
|
|||||||
keys: [13]
|
keys: [13]
|
||||||
}];
|
}];
|
||||||
|
|
||||||
var content = h('div', [
|
return {
|
||||||
list.div
|
content: div,
|
||||||
|
buttons: contactsButtons
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var friendsObject = hasFriends ? getContacts() : noContactsMessage(common);
|
||||||
|
var friendsList = friendsObject.content;
|
||||||
|
var contactsButtons = friendsObject.buttons;
|
||||||
|
contactsButtons.unshift({
|
||||||
|
className: 'cancel',
|
||||||
|
name: Messages.cancel,
|
||||||
|
onClick: function () {},
|
||||||
|
keys: [27]
|
||||||
|
});
|
||||||
|
|
||||||
|
var contactsContent = h('div.cp-share-modal', [
|
||||||
|
friendsList
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var modal = UI.dialog.customModal(content, {buttons: buttons});
|
var frameContacts = UI.dialog.customModal(contactsContent, {
|
||||||
|
buttons: contactsButtons,
|
||||||
|
});
|
||||||
|
|
||||||
|
var linkName, linkPassword, linkMessage, linkError, linkSpinText;
|
||||||
|
var linkForm, linkSpin, linkResult;
|
||||||
|
var linkWarning;
|
||||||
|
// Invite from link
|
||||||
|
var dismissButton = h('span.fa.fa-times');
|
||||||
|
var linkContent = h('div.cp-share-modal', [
|
||||||
|
h('p', Messages.team_inviteLinkTitle ),
|
||||||
|
linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}),
|
||||||
|
linkForm = h('div.cp-teams-invite-form', [
|
||||||
|
linkName = h('input', {
|
||||||
|
placeholder: Messages.team_inviteLinkTempName
|
||||||
|
}),
|
||||||
|
h('br'),
|
||||||
|
h('div.cp-teams-invite-block', [
|
||||||
|
h('span', Messages.team_inviteLinkSetPassword),
|
||||||
|
h('a.cp-teams-help.fa.fa-question-circle', {
|
||||||
|
href: origin + '/faq.html#security-pad_password',
|
||||||
|
target: "_blank",
|
||||||
|
'data-tippy-placement': "right"
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
linkPassword = UI.passwordInput({
|
||||||
|
id: 'cp-teams-invite-password',
|
||||||
|
placeholder: Messages.login_password
|
||||||
|
}),
|
||||||
|
h('div.cp-teams-invite-block',
|
||||||
|
h('span', Messages.team_inviteLinkNote)
|
||||||
|
),
|
||||||
|
linkMessage = h('textarea.cp-teams-invite-message', {
|
||||||
|
placeholder: Messages.team_inviteLinkNoteMsg,
|
||||||
|
rows: 3
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
linkSpin = h('div.cp-teams-invite-spinner', {
|
||||||
|
style: 'display: none;'
|
||||||
|
}, [
|
||||||
|
h('i.fa.fa-spinner.fa-spin'),
|
||||||
|
linkSpinText = h('span', Messages.team_inviteLinkLoading)
|
||||||
|
]),
|
||||||
|
linkResult = h('div', {
|
||||||
|
style: 'display: none;'
|
||||||
|
}, h('textarea', {
|
||||||
|
readonly: 'readonly'
|
||||||
|
})),
|
||||||
|
linkWarning = h('div.cp-teams-invite-alert.alert.alert-warning.dismissable', {
|
||||||
|
style: "display: none;"
|
||||||
|
}, [
|
||||||
|
h('span.cp-inline-alert-text', Messages.team_inviteLinkWarning),
|
||||||
|
dismissButton
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
$(linkMessage).keydown(function (e) {
|
||||||
|
if (e.which === 13) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var localStore = window.cryptpadStore;
|
||||||
|
localStore.get('hide-alert-teamInvite', function (val) {
|
||||||
|
if (val === '1') { return; }
|
||||||
|
$(linkWarning).show();
|
||||||
|
|
||||||
|
$(dismissButton).on('click', function () {
|
||||||
|
localStore.put('hide-alert-teamInvite', '1');
|
||||||
|
$(linkWarning).remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var $linkContent = $(linkContent);
|
||||||
|
var href;
|
||||||
|
var process = function () {
|
||||||
|
var $nav = $linkContent.closest('.alertify').find('nav');
|
||||||
|
$(linkError).text('').hide();
|
||||||
|
var name = $(linkName).val();
|
||||||
|
var pw = $(linkPassword).find('input').val();
|
||||||
|
var msg = $(linkMessage).val();
|
||||||
|
var hash = Hash.createRandomHash('invite', pw);
|
||||||
|
var hashData = Hash.parseTypeHash('invite', hash);
|
||||||
|
href = origin + '/teams/#' + hash;
|
||||||
|
if (!name || !name.trim()) {
|
||||||
|
$(linkError).text(Messages.team_inviteLinkErrorName).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var seeds = InviteInner.deriveSeeds(hashData.key);
|
||||||
|
var salt = InviteInner.deriveSalt(pw, AppConfig.loginSalt);
|
||||||
|
|
||||||
|
var bytes64;
|
||||||
|
NThen(function (waitFor) {
|
||||||
|
$(linkForm).hide();
|
||||||
|
$(linkSpin).show();
|
||||||
|
$nav.find('button.cp-teams-invite-create').hide();
|
||||||
|
$nav.find('button.cp-teams-invite-copy').show();
|
||||||
|
setTimeout(waitFor(), 150);
|
||||||
|
}).nThen(function (waitFor) {
|
||||||
|
InviteInner.deriveBytes(seeds.scrypt, salt, waitFor(function (_bytes) {
|
||||||
|
bytes64 = _bytes;
|
||||||
|
}));
|
||||||
|
}).nThen(function (waitFor) {
|
||||||
|
module.execCommand('CREATE_INVITE_LINK', {
|
||||||
|
name: name,
|
||||||
|
password: pw,
|
||||||
|
message: msg,
|
||||||
|
bytes64: bytes64,
|
||||||
|
hash: hash,
|
||||||
|
teamId: config.teamId,
|
||||||
|
seeds: seeds,
|
||||||
|
}, waitFor(function (obj) {
|
||||||
|
if (obj && obj.error) {
|
||||||
|
waitFor.abort();
|
||||||
|
$(linkSpin).hide();
|
||||||
|
$(linkForm).show();
|
||||||
|
$nav.find('button.cp-teams-invite-create').show();
|
||||||
|
$nav.find('button.cp-teams-invite-copy').hide();
|
||||||
|
return void $(linkError).text(Messages.team_inviteLinkError).show();
|
||||||
|
}
|
||||||
|
// Display result here
|
||||||
|
$(linkSpin).hide();
|
||||||
|
$(linkResult).show().find('textarea').text(href);
|
||||||
|
$nav.find('button.cp-teams-invite-copy').prop('disabled', '');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
var linkButtons = [{
|
||||||
|
className: 'cancel',
|
||||||
|
name: Messages.cancel,
|
||||||
|
onClick: function () {},
|
||||||
|
keys: [27]
|
||||||
|
}, {
|
||||||
|
className: 'primary cp-teams-invite-create',
|
||||||
|
name: Messages.team_inviteLinkCreate,
|
||||||
|
onClick: function () {
|
||||||
|
return process();
|
||||||
|
},
|
||||||
|
keys: []
|
||||||
|
}, {
|
||||||
|
className: 'primary cp-teams-invite-copy',
|
||||||
|
name: Messages.team_inviteLinkCopy,
|
||||||
|
onClick: function () {
|
||||||
|
if (!href) { return; }
|
||||||
|
var success = Clipboard.copy(href);
|
||||||
|
if (success) { UI.log(Messages.shareSuccess); }
|
||||||
|
},
|
||||||
|
keys: []
|
||||||
|
}];
|
||||||
|
|
||||||
|
var frameLink = UI.dialog.customModal(linkContent, {
|
||||||
|
buttons: linkButtons,
|
||||||
|
});
|
||||||
|
$(frameLink).find('.cp-teams-invite-copy').prop('disabled', 'disabled').hide();
|
||||||
|
|
||||||
|
// Create modal
|
||||||
|
var tabs = [{
|
||||||
|
title: Messages.share_contactCategory,
|
||||||
|
icon: "fa fa-address-book",
|
||||||
|
content: frameContacts,
|
||||||
|
active: hasFriends
|
||||||
|
}, {
|
||||||
|
title: Messages.share_linkCategory,
|
||||||
|
icon: "fa fa-link",
|
||||||
|
content: frameLink,
|
||||||
|
active: !hasFriends
|
||||||
|
}];
|
||||||
|
|
||||||
|
var modal = UI.dialog.tabs(tabs);
|
||||||
UI.openCustomModal(modal);
|
UI.openCustomModal(modal);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2073,12 +2256,12 @@ define([
|
|||||||
for (var k in actions) {
|
for (var k in actions) {
|
||||||
$('<button>', {
|
$('<button>', {
|
||||||
'data-type': k,
|
'data-type': k,
|
||||||
'class': 'fa ' + actions[k].icon,
|
'class': 'pure-button fa ' + actions[k].icon,
|
||||||
title: Messages['mdToolbar_' + k] || k
|
title: Messages['mdToolbar_' + k] || k
|
||||||
}).click(onClick).appendTo($toolbar);
|
}).click(onClick).appendTo($toolbar);
|
||||||
}
|
}
|
||||||
$('<button>', {
|
$('<button>', {
|
||||||
'class': 'fa fa-question cp-markdown-help',
|
'class': 'pure-button fa fa-question cp-markdown-help',
|
||||||
title: Messages.mdToolbar_help
|
title: Messages.mdToolbar_help
|
||||||
}).click(function () {
|
}).click(function () {
|
||||||
var href = Messages.mdToolbar_tutorial;
|
var href = Messages.mdToolbar_tutorial;
|
||||||
@@ -3977,7 +4160,7 @@ define([
|
|||||||
};
|
};
|
||||||
|
|
||||||
var content = h('div.cp-share-modal', [
|
var content = h('div.cp-share-modal', [
|
||||||
setHTML(h('p'), text)
|
setHTML(h('p'), text),
|
||||||
]);
|
]);
|
||||||
UI.proposal(content, todo);
|
UI.proposal(content, todo);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,11 +29,18 @@ define([
|
|||||||
};
|
};
|
||||||
|
|
||||||
var makeConfig = function (hash, opt) {
|
var makeConfig = function (hash, opt) {
|
||||||
|
var secret;
|
||||||
|
if (typeof(hash) === 'string') {
|
||||||
// We can't use cryptget with a file or a user so we can use 'pad' as hash type
|
// We can't use cryptget with a file or a user so we can use 'pad' as hash type
|
||||||
var secret = Hash.getSecrets('pad', hash, opt.password);
|
secret = Hash.getSecrets('pad', hash, opt.password);
|
||||||
|
} else if (typeof(hash) === 'object') {
|
||||||
|
// we may want to just supply options directly
|
||||||
|
// and this is the easiest place to do it
|
||||||
|
secret = hash;
|
||||||
|
}
|
||||||
if (!secret.keys) { secret.keys = secret.key; } // support old hashses
|
if (!secret.keys) { secret.keys = secret.key; } // support old hashses
|
||||||
var config = {
|
var config = {
|
||||||
websocketURL: NetConfig.getWebsocketURL(),
|
websocketURL: NetConfig.getWebsocketURL(opt.origin),
|
||||||
channel: secret.channel,
|
channel: secret.channel,
|
||||||
validateKey: secret.keys.validateKey || undefined,
|
validateKey: secret.keys.validateKey || undefined,
|
||||||
crypto: Crypto.createEncryptor(secret.keys),
|
crypto: Crypto.createEncryptor(secret.keys),
|
||||||
@@ -95,7 +102,7 @@ define([
|
|||||||
opt = opt || {};
|
opt = opt || {};
|
||||||
|
|
||||||
var config = makeConfig(hash, opt);
|
var config = makeConfig(hash, opt);
|
||||||
var Session = { cb: cb, };
|
var Session = { cb: cb, hasNetwork: Boolean(opt.network) };
|
||||||
|
|
||||||
config.onReady = function (info) {
|
config.onReady = function (info) {
|
||||||
var realtime = Session.session = info.realtime;
|
var realtime = Session.session = info.realtime;
|
||||||
@@ -105,12 +112,13 @@ define([
|
|||||||
|
|
||||||
var to = setTimeout(function () {
|
var to = setTimeout(function () {
|
||||||
cb(new Error("Timeout"));
|
cb(new Error("Timeout"));
|
||||||
}, 5000);
|
}, 15000);
|
||||||
|
|
||||||
Realtime.whenRealtimeSyncs(realtime, function () {
|
Realtime.whenRealtimeSyncs(realtime, function () {
|
||||||
clearTimeout(to);
|
clearTimeout(to);
|
||||||
|
var doc = realtime.getAuthDoc();
|
||||||
realtime.abort();
|
realtime.abort();
|
||||||
finish(Session, void 0);
|
finish(Session, void 0, doc);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
overwrite(config, opt);
|
overwrite(config, opt);
|
||||||
|
|||||||
@@ -779,6 +779,11 @@ define([
|
|||||||
postMessage("SEND_FRIEND_REQUEST", data, cb);
|
postMessage("SEND_FRIEND_REQUEST", data, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Team
|
||||||
|
common.anonGetPreviewContent = function (data, cb) {
|
||||||
|
postMessage("ANON_GET_PREVIEW_CONTENT", data, cb);
|
||||||
|
};
|
||||||
|
|
||||||
// Onlyoffice
|
// Onlyoffice
|
||||||
var onlyoffice = common.onlyoffice = {};
|
var onlyoffice = common.onlyoffice = {};
|
||||||
onlyoffice.execCommand = function (data, cb) {
|
onlyoffice.execCommand = function (data, cb) {
|
||||||
|
|||||||
54
www/common/invitation.js
Normal file
54
www/common/invitation.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
(function () {
|
||||||
|
var factory = function (Util, Nacl, Scrypt) {
|
||||||
|
var Invite = {};
|
||||||
|
|
||||||
|
Invite.deriveSeeds = function (safeSeed) {
|
||||||
|
// take the hash of the provided seed
|
||||||
|
var seed = safeSeed.replace(/\-/g, '/');
|
||||||
|
var u8_seed = Nacl.hash(Nacl.util.decodeBase64(seed));
|
||||||
|
|
||||||
|
// hash the first half again for scrypt's input
|
||||||
|
var subseed1 = Nacl.hash(u8_seed.subarray(0, 32));
|
||||||
|
// hash the remainder for the invite content
|
||||||
|
var subseed2 = Nacl.hash(u8_seed.subarray(32));
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrypt: Nacl.util.encodeBase64(subseed1),
|
||||||
|
preview: Nacl.util.encodeBase64(subseed2),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Invite.deriveSalt = function (password, instance_salt) {
|
||||||
|
return (password || '') + (instance_salt || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
// seed => bytes64
|
||||||
|
Invite.deriveBytes = function (scrypt_seed, salt, cb) {
|
||||||
|
Scrypt(scrypt_seed,
|
||||||
|
salt,
|
||||||
|
8, // memoryCost (n)
|
||||||
|
1024, // block size parameter (r)
|
||||||
|
192, // dkLen
|
||||||
|
200, // interruptStep
|
||||||
|
cb,
|
||||||
|
'base64'); // format, could be 'base64'
|
||||||
|
};
|
||||||
|
|
||||||
|
return Invite;
|
||||||
|
};
|
||||||
|
if (typeof(module) !== 'undefined' && module.exports) {
|
||||||
|
module.exports = factory(
|
||||||
|
require("../common-util"),
|
||||||
|
require("tweetnacl/nacl-fast"),
|
||||||
|
require("scrypt-async")
|
||||||
|
);
|
||||||
|
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
|
||||||
|
define([
|
||||||
|
'/common/common-util.js',
|
||||||
|
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||||
|
'/bower_components/scrypt-async/scrypt-async.min.js',
|
||||||
|
], function (Util) {
|
||||||
|
return factory(Util, window.nacl, window.scrypt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}());
|
||||||
@@ -435,8 +435,6 @@
|
|||||||
return mediaObject;
|
return mediaObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaObject.tag.innerHTML = '<img style="width: 100px; height: 100px;">';
|
|
||||||
|
|
||||||
// Download the encrypted blob
|
// Download the encrypted blob
|
||||||
download(src, function (err, u8Encrypted) {
|
download(src, function (err, u8Encrypted) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ define([
|
|||||||
'/customize/messages.js',
|
'/customize/messages.js',
|
||||||
'/common/common-util.js',
|
'/common/common-util.js',
|
||||||
'/common/common-interface.js',
|
'/common/common-interface.js',
|
||||||
|
'/common/common-ui-elements.js',
|
||||||
'/common/hyperscript.js',
|
'/common/hyperscript.js',
|
||||||
'/common/diffMarked.js',
|
'/common/diffMarked.js',
|
||||||
], function ($, Messages, Util, UI, h, DiffMd) {
|
], function ($, Messages, Util, UI, UIElements, h, DiffMd) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var debug = console.log;
|
var debug = console.log;
|
||||||
@@ -13,6 +14,8 @@ define([
|
|||||||
|
|
||||||
var MessengerUI = {};
|
var MessengerUI = {};
|
||||||
|
|
||||||
|
var mutedUsers = {};
|
||||||
|
|
||||||
var dataQuery = function (id) {
|
var dataQuery = function (id) {
|
||||||
return '[data-key="' + id + '"]';
|
return '[data-key="' + id + '"]';
|
||||||
};
|
};
|
||||||
@@ -67,8 +70,11 @@ define([
|
|||||||
h('div.cp-app-contacts-category-content')
|
h('div.cp-app-contacts-category-content')
|
||||||
]),
|
]),
|
||||||
h('div.cp-app-contacts-friends.cp-app-contacts-category', [
|
h('div.cp-app-contacts-friends.cp-app-contacts-category', [
|
||||||
h('div.cp-app-contacts-category-content'),
|
h('button.cp-app-contacts-muted-button',[
|
||||||
h('h2.cp-app-contacts-category-title', Messages.contacts_friends),
|
h('i.fa.fa-bell-slash'),
|
||||||
|
Messages.contacts_manageMuted
|
||||||
|
]),
|
||||||
|
h('div.cp-app-contacts-category-content.cp-contacts-friends')
|
||||||
]),
|
]),
|
||||||
h('div.cp-app-contacts-rooms.cp-app-contacts-category', [
|
h('div.cp-app-contacts-rooms.cp-app-contacts-category', [
|
||||||
h('div.cp-app-contacts-category-content'),
|
h('div.cp-app-contacts-category-content'),
|
||||||
@@ -184,7 +190,7 @@ define([
|
|||||||
markup.message = function (msg) {
|
markup.message = function (msg) {
|
||||||
if (msg.type !== 'MSG') { return; }
|
if (msg.type !== 'MSG') { return; }
|
||||||
var curvePublic = msg.author;
|
var curvePublic = msg.author;
|
||||||
var name = typeof msg.name !== "undefined" ?
|
var name = (typeof msg.name !== "undefined" || !contactsData[msg.author]) ?
|
||||||
(msg.name || Messages.anonymous) :
|
(msg.name || Messages.anonymous) :
|
||||||
contactsData[msg.author].displayName;
|
contactsData[msg.author].displayName;
|
||||||
var d = msg.time ? new Date(msg.time) : undefined;
|
var d = msg.time ? new Date(msg.time) : undefined;
|
||||||
@@ -486,6 +492,20 @@ define([
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var unmuteUser = function (curve) {
|
||||||
|
execCommand('UNMUTE_USER', curve, function (e) {
|
||||||
|
if (e) { return void console.error(e); }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var muteUser = function (data) {
|
||||||
|
execCommand('MUTE_USER', {
|
||||||
|
curvePublic: data.curvePublic,
|
||||||
|
name: data.displayName || data.name,
|
||||||
|
avatar: data.avatar
|
||||||
|
}, function (e /*, removed */) {
|
||||||
|
if (e) { return void console.error(e); }
|
||||||
|
});
|
||||||
|
};
|
||||||
var removeFriend = function (curvePublic) {
|
var removeFriend = function (curvePublic) {
|
||||||
execCommand('REMOVE_FRIEND', curvePublic, function (e /*, removed */) {
|
execCommand('REMOVE_FRIEND', curvePublic, function (e /*, removed */) {
|
||||||
if (e) { return void console.error(e); }
|
if (e) { return void console.error(e); }
|
||||||
@@ -496,9 +516,23 @@ define([
|
|||||||
var roomEl = h('div.cp-app-contacts-friend.cp-avatar', {
|
var roomEl = h('div.cp-app-contacts-friend.cp-avatar', {
|
||||||
'data-key': id,
|
'data-key': id,
|
||||||
'data-user': room.isFriendChat ? userlist[0].curvePublic : '',
|
'data-user': room.isFriendChat ? userlist[0].curvePublic : '',
|
||||||
title: room.name
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var curve;
|
||||||
|
if (room.isFriendChat) {
|
||||||
|
var __channel = state.channels[id];
|
||||||
|
curve = __channel.curvePublic;
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmute = h('span.cp-app-contacts-remove.fa.fa-bell.cp-unmute-icon', {
|
||||||
|
title: Messages.contacts_unmute || 'unmute',
|
||||||
|
style: (curve && mutedUsers[curve]) ? undefined : 'display: none;'
|
||||||
|
});
|
||||||
|
var mute = h('span.cp-app-contacts-remove.fa.fa-bell-slash.cp-mute-icon', {
|
||||||
|
title: Messages.contacts_mute || 'mute',
|
||||||
|
style: (curve && mutedUsers[curve]) ? 'display: none;' : undefined
|
||||||
|
});
|
||||||
var remove = h('span.cp-app-contacts-remove.fa.fa-user-times', {
|
var remove = h('span.cp-app-contacts-remove.fa.fa-user-times', {
|
||||||
title: Messages.contacts_remove
|
title: Messages.contacts_remove
|
||||||
});
|
});
|
||||||
@@ -511,8 +545,12 @@ define([
|
|||||||
});
|
});
|
||||||
var rightCol = h('span.cp-app-contacts-right-col', [
|
var rightCol = h('span.cp-app-contacts-right-col', [
|
||||||
h('span.cp-app-contacts-name', [room.name]),
|
h('span.cp-app-contacts-name', [room.name]),
|
||||||
|
h('span.cp-app-contacts-icons', [
|
||||||
|
room.isFriendChat ? mute : undefined,
|
||||||
|
room.isFriendChat ? unmute : undefined,
|
||||||
room.isFriendChat ? remove :
|
room.isFriendChat ? remove :
|
||||||
(room.isPadChat || room.isTeamChat) ? undefined : leaveRoom,
|
(room.isPadChat || room.isTeamChat) ? undefined : leaveRoom,
|
||||||
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var friendData = room.isFriendChat ? userlist[0] : {};
|
var friendData = room.isFriendChat ? userlist[0] : {};
|
||||||
@@ -523,23 +561,43 @@ define([
|
|||||||
if (friendData.profile) { window.open(origin + '/profile/#' + friendData.profile); }
|
if (friendData.profile) { window.open(origin + '/profile/#' + friendData.profile); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(unmute).on('click dblclick', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
var channel = state.channels[id];
|
||||||
|
if (!channel.isFriendChat) { return; }
|
||||||
|
var curvePublic = channel.curvePublic;
|
||||||
|
$(mute).show();
|
||||||
|
$(unmute).hide();
|
||||||
|
unmuteUser(curvePublic);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(mute).on('click dblclick', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
var channel = state.channels[id];
|
||||||
|
if (!channel.isFriendChat) { return; }
|
||||||
|
var curvePublic = channel.curvePublic;
|
||||||
|
var friend = contactsData[curvePublic] || friendData;
|
||||||
|
$(mute).hide();
|
||||||
|
$(unmute).show();
|
||||||
|
muteUser(friend);
|
||||||
|
});
|
||||||
|
|
||||||
$(remove).click(function (e) {
|
$(remove).click(function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
var channel = state.channels[id];
|
var channel = state.channels[id];
|
||||||
if (!channel.isFriendChat) { return; }
|
if (!channel.isFriendChat) { return; }
|
||||||
var curvePublic = channel.curvePublic;
|
var curvePublic = channel.curvePublic;
|
||||||
var friend = contactsData[curvePublic] || friendData;
|
var friend = contactsData[curvePublic] || friendData;
|
||||||
UI.confirm(Messages._getKey('contacts_confirmRemove', [
|
var content = h('div', [
|
||||||
Util.fixHTML(friend.name)
|
UI.setHTML(h('p'), Messages._getKey('contacts_confirmRemove', [Util.fixHTML(friend.name)])),
|
||||||
]), function (yes) {
|
]);
|
||||||
|
UI.confirm(content, function (yes) {
|
||||||
if (!yes) { return; }
|
if (!yes) { return; }
|
||||||
removeFriend(curvePublic, function (e) {
|
removeFriend(curvePublic);
|
||||||
if (e) { return void console.error(e); }
|
|
||||||
});
|
|
||||||
// TODO remove friend from userlist ui
|
// TODO remove friend from userlist ui
|
||||||
// FIXME seems to trigger EJOINED from netflux-websocket (from server);
|
// FIXME seems to trigger EJOINED from netflux-websocket (from server);
|
||||||
// (tried to join a channel in which you were already present)
|
// (tried to join a channel in which you were already present)
|
||||||
}, undefined, true);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (friendData.avatar && avatars[friendData.avatar]) {
|
if (friendData.avatar && avatars[friendData.avatar]) {
|
||||||
@@ -792,6 +850,65 @@ define([
|
|||||||
// var onJoinRoom
|
// var onJoinRoom
|
||||||
// var onLeaveRoom
|
// var onLeaveRoom
|
||||||
|
|
||||||
|
var updateMutedList = function () {
|
||||||
|
execCommand('GET_MUTED_USERS', null, function (err, muted) {
|
||||||
|
if (err) { return void console.error(err); }
|
||||||
|
mutedUsers = muted;
|
||||||
|
|
||||||
|
var $button = $userlist.find('.cp-app-contacts-muted-button');
|
||||||
|
|
||||||
|
$('.cp-app-contacts-friend[data-user]')
|
||||||
|
.find('.cp-mute-icon').show();
|
||||||
|
$('.cp-app-contacts-friend[data-user]')
|
||||||
|
.find('.cp-unmute-icon').hide();
|
||||||
|
if (!muted || Object.keys(muted).length === 0) {
|
||||||
|
$button.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = Object.keys(muted).map(function (curve) {
|
||||||
|
$('.cp-app-contacts-friend[data-user="'+curve+'"]')
|
||||||
|
.find('.cp-mute-icon').hide();
|
||||||
|
$('.cp-app-contacts-friend[data-user="'+curve+'"]')
|
||||||
|
.find('.cp-unmute-icon').show();
|
||||||
|
var data = muted[curve];
|
||||||
|
var avatar = h('span.cp-avatar');
|
||||||
|
var button = h('button', {
|
||||||
|
'data-user': curve
|
||||||
|
}, [
|
||||||
|
h('i.fa.fa-bell'),
|
||||||
|
Messages.contacts_unmute || 'unmute'
|
||||||
|
]);
|
||||||
|
UIElements.displayAvatar(common, $(avatar), data.avatar, data.name);
|
||||||
|
$(button).click(function () {
|
||||||
|
unmuteUser(curve, button);
|
||||||
|
execCommand('UNMUTE_USER', curve, function (e, data) {
|
||||||
|
if (e) { return void console.error(e); }
|
||||||
|
$(button).closest('div').remove();
|
||||||
|
if (!data) { $button.hide(); }
|
||||||
|
$('.cp-app-contacts-friend[data-user="'+curve+'"]')
|
||||||
|
.find('.cp-mute-icon').show();
|
||||||
|
if ($('.cp-contacts-muted-table').find('.cp-contacts-muted-user').length === 0) {
|
||||||
|
UI.findOKButton().click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return h('div.cp-contacts-muted-user', [
|
||||||
|
h('span', avatar),
|
||||||
|
h('span', data.name),
|
||||||
|
button
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
var content = h('div', [
|
||||||
|
h('h4', Messages.contacts_mutedUsers),
|
||||||
|
h('div.cp-contacts-muted-table', rows)
|
||||||
|
]);
|
||||||
|
$button.off('click');
|
||||||
|
$button.click(function () {
|
||||||
|
UI.alert(content);
|
||||||
|
}).show();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
var ready = false;
|
var ready = false;
|
||||||
var onMessengerReady = function () {
|
var onMessengerReady = function () {
|
||||||
@@ -806,6 +923,8 @@ define([
|
|||||||
rooms.forEach(initializeRoom);
|
rooms.forEach(initializeRoom);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateMutedList();
|
||||||
|
|
||||||
$container.removeClass('cp-app-contacts-initializing');
|
$container.removeClass('cp-app-contacts-initializing');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -882,6 +1001,10 @@ define([
|
|||||||
onUpdateData(data);
|
onUpdateData(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (cmd === 'UPDATE_MUTED') {
|
||||||
|
updateMutedList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (cmd === 'MESSAGE') {
|
if (cmd === 'MESSAGE') {
|
||||||
onMessage(data);
|
onMessage(data);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1273,6 +1273,12 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Store.anonGetPreviewContent = function (clientId, data, cb) {
|
||||||
|
Team.anonGetPreviewContent({
|
||||||
|
store: store
|
||||||
|
}, data, cb);
|
||||||
|
};
|
||||||
|
|
||||||
// Get hashes for the share button
|
// Get hashes for the share button
|
||||||
// If we can find a stronger hash
|
// If we can find a stronger hash
|
||||||
Store.getStrongerHash = function (clientId, data, _cb) {
|
Store.getStrongerHash = function (clientId, data, _cb) {
|
||||||
|
|||||||
@@ -1,79 +1,82 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var factory = function (Util, Cred, nThen) {
|
var factory = function (Util, Cred, Nacl) {
|
||||||
nThen = nThen; // XXX
|
|
||||||
var Invite = {};
|
var Invite = {};
|
||||||
|
|
||||||
/*
|
var encode64 = Nacl.util.encodeBase64;
|
||||||
TODO key derivation
|
var decode64 = Nacl.util.decodeBase64;
|
||||||
|
|
||||||
|
// ed and curve keys can be random...
|
||||||
|
Invite.generateKeys = function () {
|
||||||
|
var ed = Nacl.sign.keyPair();
|
||||||
|
var curve = Nacl.box.keyPair();
|
||||||
|
return {
|
||||||
|
edPublic: encode64(ed.publicKey),
|
||||||
|
edPrivate: encode64(ed.secretKey),
|
||||||
|
curvePublic: encode64(curve.publicKey),
|
||||||
|
curvePrivate: encode64(curve.secretKey),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Invite.generateSignPair = function () {
|
||||||
|
var ed = Nacl.sign.keyPair();
|
||||||
|
return {
|
||||||
|
validateKey: encode64(ed.publicKey),
|
||||||
|
signKey: encode64(ed.secretKey),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var b64ToChannelKeys = function (b64) {
|
||||||
|
var dispense = Cred.dispenser(decode64(b64));
|
||||||
|
return {
|
||||||
|
channel: Util.uint8ArrayToHex(dispense(16)),
|
||||||
|
cryptKey: dispense(Nacl.secretbox.keyLength),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// the secret invite values (cryptkey and channel) can be derived
|
||||||
|
// from the link seed and (optional) password
|
||||||
|
Invite.deriveInviteKeys = b64ToChannelKeys;
|
||||||
|
|
||||||
|
// the preview values (cryptkey and channel) are less sensitive than the invite values
|
||||||
|
// as they cannot be leveraged to access any further content on their own
|
||||||
|
// unless the message contains secrets.
|
||||||
|
// derived from the link seed alone.
|
||||||
|
Invite.derivePreviewKeys = b64ToChannelKeys;
|
||||||
|
|
||||||
|
Invite.createRosterEntry = function (roster, data, cb) {
|
||||||
|
var toInvite = {};
|
||||||
|
toInvite[data.curvePublic] = data.content;
|
||||||
|
roster.invite(toInvite, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* INPUTS
|
||||||
|
|
||||||
|
* password (for scrypt)
|
||||||
|
* message (personal note)
|
||||||
|
* link hash
|
||||||
|
* bytes64 (scrypt output)
|
||||||
|
* preview_hash
|
||||||
|
|
||||||
scrypt(seed, passwd) => {
|
|
||||||
curve: {
|
|
||||||
private,
|
|
||||||
public,
|
|
||||||
},
|
|
||||||
ed: {
|
|
||||||
private,
|
|
||||||
public,
|
|
||||||
}
|
|
||||||
cryptKey,
|
|
||||||
channel
|
|
||||||
}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var BYTES_REQUIRED = 256;
|
/* IO / FUNCTIONALITY
|
||||||
|
|
||||||
Invite.deriveKeys = function (seed, passwd, cb) {
|
* creator
|
||||||
cb = cb; // XXX
|
* generate a random signKey (prevent writes to preview channel)
|
||||||
// TODO validate has cb
|
* encrypt and upload the preview content
|
||||||
// TODO onceAsync the cb
|
* via CryptGet
|
||||||
// TODO cb with err if !(seed && passwd)
|
* owned by:
|
||||||
|
* the ephemeral edPublic
|
||||||
|
* the invite creator
|
||||||
|
* create a roster entry for the invitation
|
||||||
|
* with encrypted notes for the creator
|
||||||
|
* redeemer
|
||||||
|
* get the preview content
|
||||||
|
* redeem the invite
|
||||||
|
* add yourself to the roster
|
||||||
|
* add the team to your proxy-manager
|
||||||
|
|
||||||
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;
|
return Invite;
|
||||||
};
|
};
|
||||||
@@ -81,15 +84,16 @@ var factory = function (Util, Cred, nThen) {
|
|||||||
module.exports = factory(
|
module.exports = factory(
|
||||||
require("../common-util"),
|
require("../common-util"),
|
||||||
require("../common-credential.js"),
|
require("../common-credential.js"),
|
||||||
require("nthen")
|
require("nthen"),
|
||||||
|
require("tweetnacl/nacl-fast")
|
||||||
);
|
);
|
||||||
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
|
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
|
||||||
define([
|
define([
|
||||||
'/common/common-util.js',
|
'/common/common-util.js',
|
||||||
'/common/common-credential.js',
|
'/common/common-credential.js',
|
||||||
'/bower_components/nthen/index.js',
|
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||||
], function (Util, Cred, nThen) {
|
], function (Util, Cred) {
|
||||||
return factory(Util, nThen);
|
return factory(Util, Cred, window.nacl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ define([
|
|||||||
var handlers = {};
|
var handlers = {};
|
||||||
var removeHandlers = {};
|
var removeHandlers = {};
|
||||||
|
|
||||||
|
var isMuted = function (ctx, data) {
|
||||||
|
var muted = ctx.store.proxy.mutedUsers || {};
|
||||||
|
var curvePublic = Util.find(data, ['msg', 'author']);
|
||||||
|
if (!curvePublic) { return false; }
|
||||||
|
return Boolean(muted[curvePublic]);
|
||||||
|
};
|
||||||
|
|
||||||
// Store the friend request displayed to avoid duplicates
|
// Store the friend request displayed to avoid duplicates
|
||||||
var friendRequest = {};
|
var friendRequest = {};
|
||||||
handlers['FRIEND_REQUEST'] = function (ctx, box, data, cb) {
|
handlers['FRIEND_REQUEST'] = function (ctx, box, data, cb) {
|
||||||
@@ -21,6 +28,8 @@ define([
|
|||||||
return void cb(true);
|
return void cb(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMuted(ctx, data)) { return void cb(true); }
|
||||||
|
|
||||||
// Don't show duplicate friend request: if we already have a friend request
|
// Don't show duplicate friend request: if we already have a friend request
|
||||||
// in memory from the same user, dismiss the new one
|
// in memory from the same user, dismiss the new one
|
||||||
if (friendRequest[data.msg.author]) { return void cb(true); }
|
if (friendRequest[data.msg.author]) { return void cb(true); }
|
||||||
@@ -30,10 +39,22 @@ define([
|
|||||||
// If the user is already in our friend list, automatically accept the request
|
// If the user is already in our friend list, automatically accept the request
|
||||||
if (Messaging.getFriend(ctx.store.proxy, data.msg.author) ||
|
if (Messaging.getFriend(ctx.store.proxy, data.msg.author) ||
|
||||||
ctx.store.proxy.friends_pending[data.msg.author]) {
|
ctx.store.proxy.friends_pending[data.msg.author]) {
|
||||||
|
delete ctx.store.proxy.friends_pending[data.msg.author];
|
||||||
Messaging.acceptFriendRequest(ctx.store, data.msg.content, function (obj) {
|
Messaging.acceptFriendRequest(ctx.store, data.msg.content, function (obj) {
|
||||||
if (obj && obj.error) {
|
if (obj && obj.error) {
|
||||||
return void cb();
|
return void cb();
|
||||||
}
|
}
|
||||||
|
Messaging.addToFriendList({
|
||||||
|
proxy: ctx.store.proxy,
|
||||||
|
realtime: ctx.store.realtime,
|
||||||
|
pinPads: ctx.pinPads
|
||||||
|
}, data.msg.content, function (err) {
|
||||||
|
if (err) { console.error(err); }
|
||||||
|
if (ctx.store.messenger) {
|
||||||
|
ctx.store.messenger.onFriendAdded(data.msg.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.updateMetadata();
|
||||||
cb(true);
|
cb(true);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -170,6 +191,8 @@ define([
|
|||||||
var content = msg.content;
|
var content = msg.content;
|
||||||
// content.name, content.title, content.href, content.password
|
// content.name, content.title, content.href, content.password
|
||||||
|
|
||||||
|
if (isMuted(ctx, data)) { return void cb(true); }
|
||||||
|
|
||||||
var channel = Hash.hrefToHexChannelId(content.href, content.password);
|
var channel = Hash.hrefToHexChannelId(content.href, content.password);
|
||||||
var parsed = Hash.parsePadUrl(content.href);
|
var parsed = Hash.parsePadUrl(content.href);
|
||||||
var mode = parsed.hashData && parsed.hashData.mode || 'n/a';
|
var mode = parsed.hashData && parsed.hashData.mode || 'n/a';
|
||||||
@@ -212,6 +235,9 @@ define([
|
|||||||
supportMessage = true;
|
supportMessage = true;
|
||||||
cb();
|
cb();
|
||||||
};
|
};
|
||||||
|
removeHandlers['SUPPORT_MESSAGE'] = function () {
|
||||||
|
supportMessage = false;
|
||||||
|
};
|
||||||
|
|
||||||
// Incoming edit rights request: add data before sending it to inner
|
// Incoming edit rights request: add data before sending it to inner
|
||||||
handlers['REQUEST_PAD_ACCESS'] = function (ctx, box, data, cb) {
|
handlers['REQUEST_PAD_ACCESS'] = function (ctx, box, data, cb) {
|
||||||
@@ -220,6 +246,8 @@ define([
|
|||||||
|
|
||||||
if (msg.author !== content.user.curvePublic) { return void cb(true); }
|
if (msg.author !== content.user.curvePublic) { return void cb(true); }
|
||||||
|
|
||||||
|
if (isMuted(ctx, data)) { return void cb(true); }
|
||||||
|
|
||||||
var channel = content.channel;
|
var channel = content.channel;
|
||||||
var res = ctx.store.manager.findChannel(channel);
|
var res = ctx.store.manager.findChannel(channel);
|
||||||
|
|
||||||
@@ -270,6 +298,9 @@ define([
|
|||||||
var content = msg.content;
|
var content = msg.content;
|
||||||
|
|
||||||
if (msg.author !== content.user.curvePublic) { return void cb(true); }
|
if (msg.author !== content.user.curvePublic) { return void cb(true); }
|
||||||
|
|
||||||
|
if (isMuted(ctx, data)) { return void cb(true); }
|
||||||
|
|
||||||
if (!content.teamChannel && !(content.href && content.title && content.channel)) {
|
if (!content.teamChannel && !(content.href && content.title && content.channel)) {
|
||||||
console.log('Remove invalid notification');
|
console.log('Remove invalid notification');
|
||||||
return void cb(true);
|
return void cb(true);
|
||||||
@@ -327,6 +358,9 @@ define([
|
|||||||
var content = msg.content;
|
var content = msg.content;
|
||||||
|
|
||||||
if (msg.author !== content.user.curvePublic) { return void cb(true); }
|
if (msg.author !== content.user.curvePublic) { return void cb(true); }
|
||||||
|
|
||||||
|
if (isMuted(ctx, data)) { return void cb(true); }
|
||||||
|
|
||||||
if (!content.team) {
|
if (!content.team) {
|
||||||
console.log('Remove invalid notification');
|
console.log('Remove invalid notification');
|
||||||
return void cb(true);
|
return void cb(true);
|
||||||
|
|||||||
@@ -428,20 +428,21 @@ define([
|
|||||||
}
|
}
|
||||||
|
|
||||||
var channel = ctx.channels[data.channel];
|
var channel = ctx.channels[data.channel];
|
||||||
if (!channel) {
|
|
||||||
return void cb({error: "NO_SUCH_CHANNEL"});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unfriend with mailbox
|
// Unfriend with mailbox
|
||||||
if (ctx.store.mailbox && data.curvePublic && data.notifications) {
|
if (ctx.store.mailbox && data.curvePublic && data.notifications) {
|
||||||
Messaging.removeFriend(ctx.store, curvePublic, function (obj) {
|
Messaging.removeFriend(ctx.store, curvePublic, function (obj) {
|
||||||
if (obj && obj.error) { return void cb({error:obj.error}); }
|
if (obj && obj.error) { return void cb({error:obj.error}); }
|
||||||
|
ctx.updateMetadata();
|
||||||
cb(obj);
|
cb(obj);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfriend with channel
|
// Unfriend with channel
|
||||||
|
if (!channel) {
|
||||||
|
return void cb({error: "NO_SUCH_CHANNEL"});
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
var msg = [Types.unfriend, proxy.curvePublic, +new Date()];
|
var msg = [Types.unfriend, proxy.curvePublic, +new Date()];
|
||||||
var msgStr = JSON.stringify(msg);
|
var msgStr = JSON.stringify(msg);
|
||||||
@@ -458,6 +459,40 @@ define([
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var getAllClients = function (ctx) {
|
||||||
|
var all = [];
|
||||||
|
Array.prototype.push.apply(all, ctx.friendsClients);
|
||||||
|
Object.keys(ctx.channels).forEach(function (id) {
|
||||||
|
Array.prototype.push.apply(all, ctx.channels[id].clients);
|
||||||
|
});
|
||||||
|
return Util.deduplicateString(all);
|
||||||
|
};
|
||||||
|
|
||||||
|
var muteUser = function (ctx, data, _cb) {
|
||||||
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
var proxy = ctx.store.proxy;
|
||||||
|
var muted = proxy.mutedUsers = proxy.mutedUsers || {};
|
||||||
|
if (muted[data.curvePublic]) { return void cb(); }
|
||||||
|
muted[data.curvePublic] = data;
|
||||||
|
ctx.emit('UPDATE_MUTED', null, getAllClients(ctx));
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
var unmuteUser = function (ctx, curvePublic, _cb) {
|
||||||
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
var proxy = ctx.store.proxy;
|
||||||
|
var muted = proxy.mutedUsers = proxy.mutedUsers || {};
|
||||||
|
delete muted[curvePublic];
|
||||||
|
ctx.emit('UPDATE_MUTED', null, getAllClients(ctx));
|
||||||
|
cb(Object.keys(muted).length);
|
||||||
|
};
|
||||||
|
var getMutedUsers = function (ctx, cb) {
|
||||||
|
var proxy = ctx.store.proxy;
|
||||||
|
if (cb) {
|
||||||
|
return void cb(proxy.mutedUsers || {});
|
||||||
|
}
|
||||||
|
return proxy.mutedUsers || {};
|
||||||
|
};
|
||||||
|
|
||||||
var openChannel = function (ctx, data) {
|
var openChannel = function (ctx, data) {
|
||||||
var proxy = ctx.store.proxy;
|
var proxy = ctx.store.proxy;
|
||||||
var network = ctx.store.network;
|
var network = ctx.store.network;
|
||||||
@@ -664,7 +699,14 @@ define([
|
|||||||
nThen(function (waitFor) {
|
nThen(function (waitFor) {
|
||||||
// Load or get all friends channels
|
// Load or get all friends channels
|
||||||
Object.keys(friends).forEach(function (key) {
|
Object.keys(friends).forEach(function (key) {
|
||||||
if (key === 'me') { return; }
|
if (key === 'me') {
|
||||||
|
// At some point a bug inserted a friend's channel into our "me" data.
|
||||||
|
// This led to displaying our name instead of our friend's name in the
|
||||||
|
// contacts app. The following line is here to prevent this issue to happen
|
||||||
|
// again.
|
||||||
|
delete friends.me.channel;
|
||||||
|
return;
|
||||||
|
}
|
||||||
var friend = clone(friends[key]);
|
var friend = clone(friends[key]);
|
||||||
if (typeof(friend) !== 'object') { return; }
|
if (typeof(friend) !== 'object') { return; }
|
||||||
if (!friend.channel) { return; }
|
if (!friend.channel) { return; }
|
||||||
@@ -887,15 +929,6 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var getAllClients = function (ctx) {
|
|
||||||
var all = [];
|
|
||||||
Array.prototype.push.apply(all, ctx.friendsClients);
|
|
||||||
Object.keys(ctx.channels).forEach(function (id) {
|
|
||||||
Array.prototype.push.apply(all, ctx.channels[id].clients);
|
|
||||||
});
|
|
||||||
return Util.deduplicateString(all);
|
|
||||||
};
|
|
||||||
|
|
||||||
Msg.init = function (cfg, waitFor, emit) {
|
Msg.init = function (cfg, waitFor, emit) {
|
||||||
var messenger = {};
|
var messenger = {};
|
||||||
var store = cfg.store;
|
var store = cfg.store;
|
||||||
@@ -911,6 +944,9 @@ define([
|
|||||||
range_requests: {}
|
range_requests: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
store.proxy.on('change', ['mutedUsers'], function () {
|
||||||
|
ctx.emit('UPDATE_MUTED', null, getAllClients(ctx));
|
||||||
|
});
|
||||||
|
|
||||||
ctx.store.network.on('message', function(msg, sender) {
|
ctx.store.network.on('message', function(msg, sender) {
|
||||||
onDirectMessage(ctx, msg, sender);
|
onDirectMessage(ctx, msg, sender);
|
||||||
@@ -942,6 +978,12 @@ define([
|
|||||||
var channel = friend.channel;
|
var channel = friend.channel;
|
||||||
if (!channel) { return; }
|
if (!channel) { return; }
|
||||||
|
|
||||||
|
// Already friend? don't load the channel a second time
|
||||||
|
var chanId = friend.channel;
|
||||||
|
var chan = ctx.channels[chanId];
|
||||||
|
if (chan) { return; }
|
||||||
|
|
||||||
|
// Load the channel and add the friend to the contacts app
|
||||||
loadFriend(ctx, null, friend, function () {
|
loadFriend(ctx, null, friend, function () {
|
||||||
emit('FRIEND', {
|
emit('FRIEND', {
|
||||||
curvePublic: friend.curvePublic,
|
curvePublic: friend.curvePublic,
|
||||||
@@ -990,6 +1032,9 @@ define([
|
|||||||
if (cmd === 'GET_ROOMS') {
|
if (cmd === 'GET_ROOMS') {
|
||||||
return void getRooms(ctx, data, cb);
|
return void getRooms(ctx, data, cb);
|
||||||
}
|
}
|
||||||
|
if (cmd === 'GET_MUTED_USERS') {
|
||||||
|
return void getMutedUsers(ctx, cb);
|
||||||
|
}
|
||||||
if (cmd === 'GET_USERLIST') {
|
if (cmd === 'GET_USERLIST') {
|
||||||
return void getUserList(ctx, data, cb);
|
return void getUserList(ctx, data, cb);
|
||||||
}
|
}
|
||||||
@@ -1002,6 +1047,12 @@ define([
|
|||||||
if (cmd === 'REMOVE_FRIEND') {
|
if (cmd === 'REMOVE_FRIEND') {
|
||||||
return void removeFriend(ctx, data, cb);
|
return void removeFriend(ctx, data, cb);
|
||||||
}
|
}
|
||||||
|
if (cmd === 'MUTE_USER') {
|
||||||
|
return void muteUser(ctx, data, cb);
|
||||||
|
}
|
||||||
|
if (cmd === 'UNMUTE_USER') {
|
||||||
|
return void unmuteUser(ctx, data, cb);
|
||||||
|
}
|
||||||
if (cmd === 'GET_STATUS') {
|
if (cmd === 'GET_STATUS') {
|
||||||
return void getStatus(ctx, data, cb);
|
return void getStatus(ctx, data, cb);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,18 @@ define([
|
|||||||
], function (ApiConfig) {
|
], function (ApiConfig) {
|
||||||
var Config = {};
|
var Config = {};
|
||||||
|
|
||||||
Config.getWebsocketURL = function () {
|
Config.getWebsocketURL = function (origin) {
|
||||||
if (!ApiConfig.websocketPath) { return ApiConfig.websocketURL; }
|
if (!ApiConfig.websocketPath) { return ApiConfig.websocketURL; }
|
||||||
var path = ApiConfig.websocketPath;
|
var path = ApiConfig.websocketPath;
|
||||||
if (/^ws{1,2}:\/\//.test(path)) { return path; }
|
if (/^ws{1,2}:\/\//.test(path)) { return path; }
|
||||||
|
|
||||||
var protocol = window.location.protocol.replace(/http/, 'ws');
|
var l = window.location;
|
||||||
var host = window.location.host;
|
if (origin && window && window.document) {
|
||||||
|
l = document.createElement("a");
|
||||||
|
l.href = origin;
|
||||||
|
}
|
||||||
|
var protocol = l.protocol.replace(/http/, 'ws');
|
||||||
|
var host = l.host;
|
||||||
var url = protocol + '//' + host + path;
|
var url = protocol + '//' + host + path;
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
|
|||||||
@@ -364,6 +364,98 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
return changed;
|
return changed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
commands.INVITE = function (args, author, roster) {
|
||||||
|
// an invitation is created with an ephemeral curve public key
|
||||||
|
// that key is ultimately given to the user you'd like on your team
|
||||||
|
// that user can exploit their possession of the public key to remove
|
||||||
|
// the pending invitation with their actual data.
|
||||||
|
if (!isMap(args)) { throw new Error('INVALID_ARGS'); }
|
||||||
|
if (!roster.internal.initialized) { throw new Error("UNINITIALIED"); }
|
||||||
|
if (typeof(roster.state.members) === 'undefined') {
|
||||||
|
throw new Error("CANNOT+INVITE_TO_UNINITIALIED_ROSTER");
|
||||||
|
}
|
||||||
|
|
||||||
|
var members = roster.state.members;
|
||||||
|
|
||||||
|
Object.keys(args).forEach(function (curve) {
|
||||||
|
if (!isValidId(curve)) {
|
||||||
|
console.log(curve, curve.length);
|
||||||
|
throw new Error("INVALID_CURVE_KEY");
|
||||||
|
}
|
||||||
|
// reject commandws wehere the members are not proper objects
|
||||||
|
if (!isMap(args[curve])) { throw new Error("INVALID_CONTENT"); }
|
||||||
|
if (members[curve]) { throw new Error("ARLEADY_PRESENT"); }
|
||||||
|
|
||||||
|
var data = args[curve];
|
||||||
|
// if no role was provided, assume VIEWER
|
||||||
|
if (typeof(data.role) !== 'string') { data.role = "VIEWER"; }
|
||||||
|
|
||||||
|
// assume that invitations are 'pending' unless stated otherwise
|
||||||
|
if (typeof(data.pending) === 'undefined') { data.pending = true; }
|
||||||
|
|
||||||
|
if (!canAddRole(author, data.role, members)) {
|
||||||
|
throw new Error("INSUFFICIENT_PERMISSIONS");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof(data.displayName) !== 'string' || !data.displayName) { throw new Error("DISPLAYNAME_REQUIRED"); }
|
||||||
|
//if (typeof(data.notifications) !== 'string') { throw new Error("NOTIFICATIONS_REQUIRED"); }
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
<ephemeralCurveKey>: {
|
||||||
|
role: ??? || 'VIEWER',
|
||||||
|
displayName: '',
|
||||||
|
pending: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
|
|
||||||
|
Object.keys(args).forEach(function (curve) {
|
||||||
|
changed = true;
|
||||||
|
members[curve] = args[curve];
|
||||||
|
});
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
};
|
||||||
|
|
||||||
|
commands.ACCEPT = function (args, author, roster) {
|
||||||
|
if (!roster.internal.initialized) { throw new Error("UNINITIALIED"); }
|
||||||
|
if (typeof(roster.state.members) === 'undefined') {
|
||||||
|
throw new Error("CANNOT_ADD_TO_UNINITIALIED_ROSTER");
|
||||||
|
}
|
||||||
|
|
||||||
|
// an ACCEPT command replaces a pending invitation's curve key with a new one
|
||||||
|
// after which the invited member can use their actual curve key to describe themselves
|
||||||
|
|
||||||
|
// the author must have been invited already...
|
||||||
|
var members = roster.state.members;
|
||||||
|
|
||||||
|
// so you must already be in the members list
|
||||||
|
if (!isMap(members[author])) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
|
||||||
|
// and your membership must indicate that you are 'pending'
|
||||||
|
if (!members[author].pending) { throw new Error("ALREADY_PRESENT"); }
|
||||||
|
|
||||||
|
// args should be a string
|
||||||
|
if (typeof(args) !== 'string') { throw new Error("INVALID_ARGS"); }
|
||||||
|
// ...and a valid curve key
|
||||||
|
if (!isValidId(args)) { throw new Error("INVALID_CURVE_KEY"); }
|
||||||
|
|
||||||
|
var curve = args;
|
||||||
|
|
||||||
|
// and the curve key must not already be a member
|
||||||
|
if (typeof(members[curve]) !== 'undefined') { throw new Error("MEMBER_ALREADY_PRESENT"); }
|
||||||
|
|
||||||
|
// copy the new profile from the old one
|
||||||
|
members[curve] = Util.clone(members[author]);
|
||||||
|
// and erase the old one
|
||||||
|
delete members[author];
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
var handleCommand = function (content, author, roster) {
|
var handleCommand = function (content, author, roster) {
|
||||||
if (!(Array.isArray(content) && typeof(author) === 'string')) {
|
if (!(Array.isArray(content) && typeof(author) === 'string')) {
|
||||||
throw new Error("INVALID ARGUMENTS");
|
throw new Error("INVALID ARGUMENTS");
|
||||||
@@ -671,6 +763,31 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
send(['METADATA', data], cb);
|
send(['METADATA', data], cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// supports multiple invite
|
||||||
|
roster.invite = function (_data, _cb) {
|
||||||
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
var state = ref.state;
|
||||||
|
if (!state) { return cb("UNINITIALIZED"); }
|
||||||
|
if (!ref.internal.initialized) { return cb("UNINITIALIZED"); }
|
||||||
|
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
|
||||||
|
var data = Util.clone(_data);
|
||||||
|
|
||||||
|
Object.keys(data).forEach(function (curve) {
|
||||||
|
if (!isValidId(curve) || isMap(ref.state.members[curve])) { return delete data[curve]; }
|
||||||
|
});
|
||||||
|
|
||||||
|
send(['INVITE', data], cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
roster.accept = function (_data, _cb) {
|
||||||
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
if (typeof(_data) !== 'string' || !isValidId(_data)) {
|
||||||
|
return void cb("INVALID_ARGUMENTS");
|
||||||
|
}
|
||||||
|
|
||||||
|
send([ 'ACCEPT', _data ], cb);
|
||||||
|
};
|
||||||
|
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
// get metadata so we know the owners and validateKey
|
// get metadata so we know the owners and validateKey
|
||||||
anon_rpc.send('GET_METADATA', channel, function (err, data) {
|
anon_rpc.send('GET_METADATA', channel, function (err, data) {
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ define([
|
|||||||
// Messaging
|
// Messaging
|
||||||
ANSWER_FRIEND_REQUEST: Store.answerFriendRequest,
|
ANSWER_FRIEND_REQUEST: Store.answerFriendRequest,
|
||||||
SEND_FRIEND_REQUEST: Store.sendFriendRequest,
|
SEND_FRIEND_REQUEST: Store.sendFriendRequest,
|
||||||
|
// Team invitation
|
||||||
|
ANON_GET_PREVIEW_CONTENT: Store.anonGetPreviewContent,
|
||||||
// OnlyOffice
|
// OnlyOffice
|
||||||
OO_COMMAND: Store.onlyoffice.execCommand,
|
OO_COMMAND: Store.onlyoffice.execCommand,
|
||||||
// Cursor
|
// Cursor
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ define([
|
|||||||
'/common/outer/roster.js',
|
'/common/outer/roster.js',
|
||||||
'/common/common-messaging.js',
|
'/common/common-messaging.js',
|
||||||
'/common/common-feedback.js',
|
'/common/common-feedback.js',
|
||||||
|
'/common/outer/invitation.js',
|
||||||
|
'/common/cryptget.js',
|
||||||
|
|
||||||
'/bower_components/chainpad-listmap/chainpad-listmap.js',
|
'/bower_components/chainpad-listmap/chainpad-listmap.js',
|
||||||
'/bower_components/chainpad-crypto/crypto.js',
|
'/bower_components/chainpad-crypto/crypto.js',
|
||||||
@@ -19,7 +21,7 @@ define([
|
|||||||
'/bower_components/saferphore/index.js',
|
'/bower_components/saferphore/index.js',
|
||||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||||
], function (Util, Hash, Constants, Realtime,
|
], function (Util, Hash, Constants, Realtime,
|
||||||
ProxyManager, UserObject, SF, Roster, Messaging, Feedback,
|
ProxyManager, UserObject, SF, Roster, Messaging, Feedback, Invite, Crypt,
|
||||||
Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) {
|
Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) {
|
||||||
var Team = {};
|
var Team = {};
|
||||||
|
|
||||||
@@ -137,6 +139,15 @@ define([
|
|||||||
if (membersChannel) { list.push(membersChannel); }
|
if (membersChannel) { list.push(membersChannel); }
|
||||||
if (mailboxChannel) { list.push(mailboxChannel); }
|
if (mailboxChannel) { list.push(mailboxChannel); }
|
||||||
|
|
||||||
|
var state = store.roster.getState();
|
||||||
|
if (state.members) {
|
||||||
|
Object.keys(state.members).forEach(function (curve) {
|
||||||
|
var m = state.members[curve];
|
||||||
|
if (m.inviteChannel && m.pending) { list.push(m.inviteChannel); }
|
||||||
|
if (m.previewChannel && m.pending) { list.push(m.previewChannel); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
list.sort();
|
list.sort();
|
||||||
return list;
|
return list;
|
||||||
};
|
};
|
||||||
@@ -1259,6 +1270,290 @@ define([
|
|||||||
ctx.store.messenger.openTeamChat(team.getChatData(), onUpdate, cId, cb);
|
ctx.store.messenger.openTeamChat(team.getChatData(), onUpdate, cId, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var createInviteLink = function (ctx, data, cId, _cb) {
|
||||||
|
var cb = Util.mkAsync(Util.once(_cb));
|
||||||
|
|
||||||
|
var teamId = data.teamId;
|
||||||
|
var team = ctx.teams[data.teamId];
|
||||||
|
var seeds = data.seeds; // {scrypt, preview}
|
||||||
|
var bytes64 = data.bytes64;
|
||||||
|
|
||||||
|
if (!teamId || !team) { return void cb({error: 'EINVAL'}); }
|
||||||
|
|
||||||
|
var roster = team.roster;
|
||||||
|
|
||||||
|
var teamName;
|
||||||
|
try {
|
||||||
|
teamName = roster.getState().metadata.name;
|
||||||
|
} catch (err) {
|
||||||
|
return void cb({ error: "TEAM_NAME_ERR" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = data.message;
|
||||||
|
var name = data.name;
|
||||||
|
|
||||||
|
/*
|
||||||
|
var password = data.password;
|
||||||
|
var hash = data.hash;
|
||||||
|
*/
|
||||||
|
|
||||||
|
// derive { channel, cryptKey} for the preview content channel
|
||||||
|
var previewKeys = Invite.derivePreviewKeys(seeds.preview);
|
||||||
|
|
||||||
|
// derive {channel, cryptkey} for the invite content channel
|
||||||
|
var inviteKeys = Invite.deriveInviteKeys(bytes64);
|
||||||
|
|
||||||
|
// randomly generate ephemeral keys for ownership of the above content
|
||||||
|
// and a placeholder in the roster
|
||||||
|
var ephemeralKeys = Invite.generateKeys();
|
||||||
|
|
||||||
|
nThen(function (w) {
|
||||||
|
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
// a random signing keypair to prevent further writes to the channel
|
||||||
|
// we don't need to remember it cause we're only writing once
|
||||||
|
var sign = Invite.generateSignPair(); // { validateKey, signKey}
|
||||||
|
var putOpts = {
|
||||||
|
initialState: '{}',
|
||||||
|
network: ctx.store.network,
|
||||||
|
metadata: {
|
||||||
|
owners: [ctx.store.proxy.edPublic, ephemeralKeys.edPublic]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
putOpts.metadata.validateKey = sign.validateKey;
|
||||||
|
|
||||||
|
// visible with only the invite link
|
||||||
|
var previewContent = {
|
||||||
|
teamName: teamName,
|
||||||
|
message: message,
|
||||||
|
author: Messaging.createData(ctx.store.proxy, false),
|
||||||
|
displayName: name,
|
||||||
|
};
|
||||||
|
|
||||||
|
var cryptput_config = {
|
||||||
|
channel: previewKeys.channel,
|
||||||
|
type: 'pad',
|
||||||
|
version: 2,
|
||||||
|
keys: { // what would normally be provided by getSecrets
|
||||||
|
cryptKey: previewKeys.cryptKey,
|
||||||
|
validateKey: sign.validateKey, // sent to historyKeeper
|
||||||
|
signKey: sign.signKey, // b64EdPrivate
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Crypt.put(cryptput_config, JSON.stringify(previewContent), w(function (err /*, doc */) {
|
||||||
|
if (err) {
|
||||||
|
console.error("CRYPTPUT_ERR", err);
|
||||||
|
w.abort();
|
||||||
|
return void cb({ error: "SET_PREVIEW_CONTENT" });
|
||||||
|
}
|
||||||
|
}), putOpts);
|
||||||
|
}());
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
// a different random signing key so that the server can't correlate these documents
|
||||||
|
// as components of an invite
|
||||||
|
var sign = Invite.generateSignPair(); // { validateKey, signKey}
|
||||||
|
var putOpts = {
|
||||||
|
initialState: '{}',
|
||||||
|
network: ctx.store.network,
|
||||||
|
metadata: {
|
||||||
|
owners: [ctx.store.proxy.edPublic, ephemeralKeys.edPublic]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
putOpts.metadata.validateKey = sign.validateKey;
|
||||||
|
|
||||||
|
// available only with the link and the content
|
||||||
|
var inviteContent = {
|
||||||
|
teamData: getInviteData(ctx, teamId, false),
|
||||||
|
ephemeral: {
|
||||||
|
edPublic: ephemeralKeys.edPublic,
|
||||||
|
edPrivate: ephemeralKeys.edPrivate,
|
||||||
|
curvePublic: ephemeralKeys.curvePublic,
|
||||||
|
curvePrivate: ephemeralKeys.curvePrivate,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var cryptput_config = {
|
||||||
|
channel: inviteKeys.channel,
|
||||||
|
type: 'pad',
|
||||||
|
version: 2,
|
||||||
|
keys: {
|
||||||
|
cryptKey: inviteKeys.cryptKey,
|
||||||
|
validateKey: sign.validateKey,
|
||||||
|
signKey: sign.signKey,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Crypt.put(cryptput_config, JSON.stringify(inviteContent), w(function (err /*, doc */) {
|
||||||
|
if (err) {
|
||||||
|
console.error("CRYPTPUT_ERR", err);
|
||||||
|
w.abort();
|
||||||
|
return void cb({ error: "SET_PREVIEW_CONTENT" });
|
||||||
|
}
|
||||||
|
}), putOpts);
|
||||||
|
}());
|
||||||
|
}).nThen(function (w) {
|
||||||
|
team.pin([inviteKeys.channel, previewKeys.channel], function (obj) {
|
||||||
|
if (obj && obj.error) { console.error(obj.error); }
|
||||||
|
});
|
||||||
|
Invite.createRosterEntry(team.roster, {
|
||||||
|
curvePublic: ephemeralKeys.curvePublic,
|
||||||
|
content: {
|
||||||
|
curvePublic: ephemeralKeys.curvePublic,
|
||||||
|
displayName: data.name,
|
||||||
|
pending: true,
|
||||||
|
inviteChannel: inviteKeys.channel,
|
||||||
|
previewChannel: previewKeys.channel,
|
||||||
|
}
|
||||||
|
}, w(function (err) {
|
||||||
|
if (err) {
|
||||||
|
w.abort();
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}).nThen(function () {
|
||||||
|
// call back empty if everything worked
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var getPreviewContent = function (ctx, data, cId, cb) {
|
||||||
|
var seeds = data.seeds;
|
||||||
|
var previewKeys;
|
||||||
|
try {
|
||||||
|
previewKeys = Invite.derivePreviewKeys(seeds.preview);
|
||||||
|
} catch (err) {
|
||||||
|
return void cb({ error: "INVALID_SEEDS" });
|
||||||
|
}
|
||||||
|
Crypt.get({ // secrets
|
||||||
|
channel: previewKeys.channel,
|
||||||
|
type: 'pad',
|
||||||
|
version: 2,
|
||||||
|
keys: {
|
||||||
|
cryptKey: previewKeys.cryptKey,
|
||||||
|
},
|
||||||
|
}, function (err, val) {
|
||||||
|
if (err) { return void cb({ error: err }); }
|
||||||
|
if (!val) { return void cb({ error: 'DELETED' }); }
|
||||||
|
|
||||||
|
var json = Util.tryParse(val);
|
||||||
|
if (!json) { return void cb({ error: "parseError" }); }
|
||||||
|
console.error("JSON", json);
|
||||||
|
cb(json);
|
||||||
|
}, { // cryptget opts
|
||||||
|
network: ctx.store.network,
|
||||||
|
initialState: '{}',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var getInviteContent = function (ctx, data, cId, cb) {
|
||||||
|
var bytes64 = data.bytes64;
|
||||||
|
var previewKeys;
|
||||||
|
try {
|
||||||
|
previewKeys = Invite.deriveInviteKeys(bytes64);
|
||||||
|
} catch (err) {
|
||||||
|
return void cb({ error: "INVALID_SEEDS" });
|
||||||
|
}
|
||||||
|
Crypt.get({ // secrets
|
||||||
|
channel: previewKeys.channel,
|
||||||
|
type: 'pad',
|
||||||
|
version: 2,
|
||||||
|
keys: {
|
||||||
|
cryptKey: previewKeys.cryptKey,
|
||||||
|
},
|
||||||
|
}, function (err, val) {
|
||||||
|
if (err) { return void cb({error: err}); }
|
||||||
|
if (!val) { return void cb({error: 'DELETED'}); }
|
||||||
|
|
||||||
|
var json = Util.tryParse(val);
|
||||||
|
if (!json) { return void cb({error: "parseError"}); }
|
||||||
|
cb(json);
|
||||||
|
}, { // cryptget opts
|
||||||
|
network: ctx.store.network,
|
||||||
|
initialState: '{}',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var acceptLinkInvitation = function (ctx, data, cId, cb) {
|
||||||
|
var inviteContent;
|
||||||
|
var rosterState;
|
||||||
|
nThen(function (waitFor) {
|
||||||
|
// Get team keys and ephemeral keys
|
||||||
|
getInviteContent(ctx, data, cId, waitFor(function (obj) {
|
||||||
|
if (obj && obj.error) {
|
||||||
|
waitFor.abort();
|
||||||
|
return void cb(obj);
|
||||||
|
}
|
||||||
|
inviteContent = obj;
|
||||||
|
}));
|
||||||
|
}).nThen(function (waitFor) {
|
||||||
|
// Check if you're already a member of this team
|
||||||
|
var chan = Util.find(inviteContent, ['teamData', 'channel']);
|
||||||
|
var myTeams = ctx.store.proxy.teams || {};
|
||||||
|
var isMember = Object.keys(myTeams).some(function (k) {
|
||||||
|
var t = myTeams[k];
|
||||||
|
return t.channel === chan;
|
||||||
|
});
|
||||||
|
if (isMember) {
|
||||||
|
waitFor.abort();
|
||||||
|
return void cb({error: 'ALREADY_MEMBER'});
|
||||||
|
}
|
||||||
|
// Accept the roster invitation: relplace our ephemeral keys with our user keys
|
||||||
|
var rosterData = Util.find(inviteContent, ['teamData', 'keys', 'roster']);
|
||||||
|
var myKeys = inviteContent.ephemeral;
|
||||||
|
if (!rosterData || !myKeys) {
|
||||||
|
waitFor.abort();
|
||||||
|
return void cb({error: 'INVALID_INVITE_CONTENT'});
|
||||||
|
}
|
||||||
|
var rosterKeys = Crypto.Team.deriveMemberKeys(rosterData.edit, myKeys);
|
||||||
|
Roster.create({
|
||||||
|
network: ctx.store.network,
|
||||||
|
channel: rosterData.channel,
|
||||||
|
keys: rosterKeys,
|
||||||
|
anon_rpc: ctx.store.anon_rpc,
|
||||||
|
}, waitFor(function (err, roster) {
|
||||||
|
if (err) {
|
||||||
|
waitFor.abort();
|
||||||
|
console.error(err);
|
||||||
|
return void cb({error: 'ROSTER_ERROR'});
|
||||||
|
}
|
||||||
|
var myData = Messaging.createData(ctx.store.proxy, false);
|
||||||
|
var state = roster.getState();
|
||||||
|
rosterState = state.members[myKeys.curvePublic];
|
||||||
|
roster.accept(myData.curvePublic, waitFor(function (err) {
|
||||||
|
roster.stop();
|
||||||
|
if (err) {
|
||||||
|
waitFor.abort();
|
||||||
|
console.error(err);
|
||||||
|
return void cb({error: 'ACCEPT_ERROR'});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}).nThen(function () {
|
||||||
|
var tempRpc = {};
|
||||||
|
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
|
||||||
|
if (err) { return; }
|
||||||
|
var rpc = tempRpc.rpc;
|
||||||
|
if (rosterState.inviteChannel) {
|
||||||
|
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
|
||||||
|
if (err) { console.error(err); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (rosterState.previewChannel) {
|
||||||
|
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
|
||||||
|
if (err) { console.error(err); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Add the team to our list and join...
|
||||||
|
joinTeam(ctx, {
|
||||||
|
team: inviteContent.teamData
|
||||||
|
}, cId, cb);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
Team.init = function (cfg, waitFor, emit) {
|
Team.init = function (cfg, waitFor, emit) {
|
||||||
var team = {};
|
var team = {};
|
||||||
var store = cfg.store;
|
var store = cfg.store;
|
||||||
@@ -1412,11 +1707,24 @@ define([
|
|||||||
if (cmd === 'GET_EDITABLE_FOLDERS') {
|
if (cmd === 'GET_EDITABLE_FOLDERS') {
|
||||||
return void getEditableFolders(ctx, data, clientId, cb);
|
return void getEditableFolders(ctx, data, clientId, cb);
|
||||||
}
|
}
|
||||||
|
if (cmd === 'CREATE_INVITE_LINK') {
|
||||||
|
return void createInviteLink(ctx, data, clientId, cb);
|
||||||
|
}
|
||||||
|
if (cmd === 'GET_PREVIEW_CONTENT') {
|
||||||
|
return void getPreviewContent(ctx, data, clientId, cb);
|
||||||
|
}
|
||||||
|
if (cmd === 'ACCEPT_LINK_INVITATION') {
|
||||||
|
return void acceptLinkInvitation(ctx, data, clientId, cb);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return team;
|
return team;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Team.anonGetPreviewContent = function (cfg, data, cb) {
|
||||||
|
getPreviewContent(cfg, data, null, cb);
|
||||||
|
};
|
||||||
|
|
||||||
return Team;
|
return Team;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -675,6 +675,10 @@ define([
|
|||||||
Cryptpad.messaging.answerFriendRequest(data, cb);
|
Cryptpad.messaging.answerFriendRequest(data, cb);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sframeChan.on('Q_ANON_GET_PREVIEW_CONTENT', function (data, cb) {
|
||||||
|
Cryptpad.anonGetPreviewContent(data, cb);
|
||||||
|
});
|
||||||
|
|
||||||
// History
|
// History
|
||||||
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) {
|
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) {
|
||||||
var crypto = Crypto.createEncryptor(secret.keys);
|
var crypto = Crypto.createEncryptor(secret.keys);
|
||||||
|
|||||||
@@ -1251,5 +1251,35 @@
|
|||||||
"share_linkPasswordAlert": "Dieses Element ist passwortgeschützt. Wenn du diesen Link teilst, muss der Empfänger das Passwort eingeben.",
|
"share_linkPasswordAlert": "Dieses Element ist passwortgeschützt. Wenn du diesen Link teilst, muss der Empfänger das Passwort eingeben.",
|
||||||
"share_contactPasswordAlert": "Dieses Element ist passwortgeschützt. Weil du es mit einem CryptPad-Kontakt teilst, muss der Empfänger das Passwort nicht eingeben.",
|
"share_contactPasswordAlert": "Dieses Element ist passwortgeschützt. Weil du es mit einem CryptPad-Kontakt teilst, muss der Empfänger das Passwort nicht eingeben.",
|
||||||
"share_embedPasswordAlert": "Dieses Element ist passwortgeschützt. Wenn du dieses Pad einbettest, werden Betrachter nach dem Passwort gefragt.",
|
"share_embedPasswordAlert": "Dieses Element ist passwortgeschützt. Wenn du dieses Pad einbettest, werden Betrachter nach dem Passwort gefragt.",
|
||||||
"passwordFaqLink": "Mehr über Passwörter erfahren"
|
"passwordFaqLink": "Mehr über Passwörter erfahren",
|
||||||
|
"share_noContactsLoggedIn": "Du hast noch keine Kontakte bei CryptPad. Teile den Link zu deinem Profil, damit andere dir Kontaktanfragen senden können.",
|
||||||
|
"share_copyProfileLink": "Profil-Link kopieren",
|
||||||
|
"share_noContactsNotLoggedIn": "Logge dich ein oder registriere dich, um deine Kontakte zu sehen und neue hinzuzufügen.",
|
||||||
|
"contacts_mute": "Stummschalten",
|
||||||
|
"contacts_unmute": "Stummschaltung aufheben",
|
||||||
|
"contacts_manageMuted": "Stummschaltungen verwalten",
|
||||||
|
"contacts_mutedUsers": "Stummgeschaltete Accounts",
|
||||||
|
"contacts_muteInfo": "Du erhältst keine Benachrichtigungen oder Mitteilungen von stummgeschalteten Nutzern.<br>Sie werden nicht erfahren, dass du sie stummgeschaltet hast. ",
|
||||||
|
"team_inviteLinkTitle": "Erstelle eine personalisierte Einladung für dieses Team",
|
||||||
|
"team_inviteLinkTempName": "Vorläufiger Name (sichtbar in der Liste ausstehender Einladungen)",
|
||||||
|
"team_inviteLinkSetPassword": "Link mit einem Passwort schützen (empfohlen)",
|
||||||
|
"team_inviteLinkNote": "Persönliche Nachricht hinzufügen",
|
||||||
|
"team_inviteLinkNoteMsg": "Diese Nachricht wird angezeigt, bevor der Empfänger entscheidet, ob er diesem Team beitreten möchte.",
|
||||||
|
"team_inviteLinkLoading": "Dein Link wird generiert",
|
||||||
|
"team_inviteLinkWarning": "Die erste Person, die auf diesen Link zugreift, kann diesem Team beitreten und dessen Inhalte einsehen. Teile ihn sorgfältig.",
|
||||||
|
"team_inviteLinkErrorName": "Bitte gib einen Namen für die eingeladene Person ein. Er kann später geändert werden. ",
|
||||||
|
"team_inviteLinkCreate": "Link erstellen",
|
||||||
|
"team_inviteLinkCopy": "Link kopieren",
|
||||||
|
"team_inviteFrom": "Von:",
|
||||||
|
"team_inviteFromMsg": "{0} hat dich ins Team <b>{1}</b> eingeladen",
|
||||||
|
"team_invitePleaseLogin": "Bitte logge dich ein oder registriere dich, um diese Einladung anzunehmen.",
|
||||||
|
"team_inviteEnterPassword": "Bitte gib das Passwort für die Einladung ein, um fortzufahren.",
|
||||||
|
"team_invitePasswordLoading": "Einladung wird entschlüsselt",
|
||||||
|
"team_inviteJoin": "Team beitreten",
|
||||||
|
"team_inviteTitle": "Team-Einladung",
|
||||||
|
"team_inviteGetData": "Team-Daten werden abgerufen",
|
||||||
|
"team_cat_link": "Einladungslink",
|
||||||
|
"team_links": "Einladungslinks",
|
||||||
|
"team_inviteInvalidLinkError": "Dieser Einladungslink ist ungültig.",
|
||||||
|
"team_inviteLinkError": "Bei der Erstellung des Links ist ein Fehler aufgetreten."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,37 @@
|
|||||||
{
|
{
|
||||||
|
"type": {
|
||||||
|
"pad": "Teksti",
|
||||||
|
"code": "Koodi",
|
||||||
|
"poll": "Kysely",
|
||||||
|
"kanban": "Kanban",
|
||||||
|
"slide": "Esitys",
|
||||||
|
"drive": "CryptDrive",
|
||||||
|
"whiteboard": "Tussitaulu",
|
||||||
|
"file": "Tiedosto",
|
||||||
|
"media": "Media",
|
||||||
|
"todo": "Tehtävälista",
|
||||||
|
"contacts": "Yhteystiedot",
|
||||||
|
"sheet": "Taulukko (Beta)",
|
||||||
|
"teams": "Teams"
|
||||||
|
},
|
||||||
|
"button_newpad": "Uusi Teksti-padi",
|
||||||
|
"button_newcode": "Uusi Koodi-padi",
|
||||||
|
"button_newpoll": "Uusi Kysely",
|
||||||
|
"button_newslide": "Uusi Esitys",
|
||||||
|
"button_newwhiteboard": "Uusi Tussitaulu",
|
||||||
|
"button_newkanban": "Uusi Kanban",
|
||||||
|
"button_newsheet": "Uusi Taulukko",
|
||||||
|
"common_connectionLost": "<b>Yhteys palvelimelle katkennut</b><br>Sovellus on vain luku-tilassa, kunnes yhteys palaa.",
|
||||||
|
"websocketError": "Yhdistäminen websocket-palvelimelle epäonnistui...",
|
||||||
|
"typeError": "Tämä padi ei ole yhteensopiva valitun sovelluksen kanssa",
|
||||||
|
"onLogout": "Olet kirjautunut ulos, {0}klikkaa tästä{1} kirjautuaksesi sisään tai paina <em>Esc-näppäintä</em> käyttääksesi padia vain luku-tilassa.",
|
||||||
|
"wrongApp": "Reaaliaikaisen sisällön näyttäminen selaimessa epäonnistui. Ole hyvä ja yritä sivun lataamista uudelleen.",
|
||||||
|
"padNotPinned": "Tämä padi vanhenee kolmen kuukauden käyttämättömyyden jälkeen, {0}kirjaudu sisään{1} tai [2}rekisteröidy{3} säilyttääksesi sen.",
|
||||||
|
"padNotPinnedVariable": "Tämä padi vanhenee {4} päivän käyttämättömyyden jälkeen, {0}kirjaudu sisään{1} tai {2}rekisteröidy{3} säilyttääksesi sen.",
|
||||||
|
"anonymousStoreDisabled": "Tämän CryptPad-instanssin ylläpitäjä on estänyt anonyymien käyttäjien pääsyn tallennustilaan. Kirjaudu sisään käyttääksesi CryptDrivea.",
|
||||||
|
"expiredError": "Tämä padi on vanhentunut, eikä se ole enää saatavilla.",
|
||||||
|
"deletedError": "Tämä padi on poistettu omistajansa toimesta, eikä se ole enää saatavilla.",
|
||||||
|
"inactiveError": "Tämä padi on poistettu käyttämättömyyden vuoksi. Paina Esc-näppäintä luodaksesi uuden padin.",
|
||||||
|
"chainpadError": "Sisältöä päivitettäessä tapahtui vakava virhe. Tämä sivu on vain luku-tilassa, jotta tekemäsi muutokset eivät katoaisi.<br>Paina <em>Esc-näppäintä</em> jatkaaksesi padin katselua vain luku-tilassa, tai lataa sivu uudelleen yrittääksesi muokkaamista.",
|
||||||
|
"invalidHashError": "Pyytämäsi dokumentin URL-osoite on virheellinen."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1254,5 +1254,32 @@
|
|||||||
"passwordFaqLink": "En lire plus sur les mots de passe",
|
"passwordFaqLink": "En lire plus sur les mots de passe",
|
||||||
"share_noContactsLoggedIn": "Vous n'avez pas encore ajouté de contacts sur CryptPad. Partagez le lien de votre profil pour que l'on vous envoie des demandes de contact.",
|
"share_noContactsLoggedIn": "Vous n'avez pas encore ajouté de contacts sur CryptPad. Partagez le lien de votre profil pour que l'on vous envoie des demandes de contact.",
|
||||||
"share_copyProfileLink": "Copier le lien du profil",
|
"share_copyProfileLink": "Copier le lien du profil",
|
||||||
"share_noContactsNotLoggedIn": "Connectez-vous ou enregistrez-vous pour voir vos contacts ou en ajouter de nouveaux."
|
"share_noContactsNotLoggedIn": "Connectez-vous ou enregistrez-vous pour voir vos contacts ou en ajouter de nouveaux.",
|
||||||
|
"contacts_mute": "Masquer",
|
||||||
|
"contacts_unmute": "Réafficher",
|
||||||
|
"contacts_manageMuted": "Comptes masqués",
|
||||||
|
"contacts_mutedUsers": "Comptes masqués",
|
||||||
|
"contacts_muteInfo": "Vous ne receverez plus de notifications ou de messages si vous masquez ce compte.<br>L'utilisateur ne sera pas informé que vous l'avez masqué. ",
|
||||||
|
"team_inviteLinkTitle": "Créer une invitation personnalisée à cette équipe",
|
||||||
|
"team_inviteLinkTempName": "Nom temporaire (apparaît dans la liste des invitations en cours)",
|
||||||
|
"team_inviteLinkSetPassword": "Protéger le lien avec un mot de passe (recommandé)",
|
||||||
|
"team_inviteLinkNote": "Ajouter un message personnalisé",
|
||||||
|
"team_inviteLinkNoteMsg": "Ce message sera affiché avant que le destinataire décide de rejoindre cette équipe.",
|
||||||
|
"team_inviteLinkLoading": "Lien en cours de création",
|
||||||
|
"team_inviteLinkWarning": "La première personne qui accédera à ce lien pourra devenir membre de l'équipe et voir son contenu. Partagez le avec prudence.",
|
||||||
|
"team_inviteLinkErrorName": "Merci de remplir un nom de la personne que vous invitez. Ce nom pourra être changé . ",
|
||||||
|
"team_inviteLinkCreate": "Créer le lien",
|
||||||
|
"team_inviteLinkCopy": "Copier le lien",
|
||||||
|
"team_inviteFrom": "De :",
|
||||||
|
"team_inviteFromMsg": "{0} vous a invité à rejoindre l'équipe <b>{1}</b>",
|
||||||
|
"team_invitePleaseLogin": "Merci de vous inscrire ou de vous connecter pour accepter cette invitation.",
|
||||||
|
"team_inviteEnterPassword": "Entrez le mot de passe pour continuer.",
|
||||||
|
"team_invitePasswordLoading": "Déchiffrement de l'invitation",
|
||||||
|
"team_inviteJoin": "Rejoindre l'équipe",
|
||||||
|
"team_inviteTitle": "Invitation à une équipe",
|
||||||
|
"team_inviteGetData": "Obtention des données de l'équipe",
|
||||||
|
"team_cat_link": "Invitation",
|
||||||
|
"team_links": "Liens d'invitation",
|
||||||
|
"team_inviteInvalidLinkError": "Ce lien d'invitation n'est pas valide.",
|
||||||
|
"team_inviteLinkError": "Erreur lors de la génération du lien."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1254,5 +1254,32 @@
|
|||||||
"passwordFaqLink": "Read more about passwords",
|
"passwordFaqLink": "Read more about passwords",
|
||||||
"share_noContactsLoggedIn": "You are not connected with anyone on CryptPad yet. Share the link to your profile for people to send you contact requests.",
|
"share_noContactsLoggedIn": "You are not connected with anyone on CryptPad yet. Share the link to your profile for people to send you contact requests.",
|
||||||
"share_copyProfileLink": "Copy profile link",
|
"share_copyProfileLink": "Copy profile link",
|
||||||
"share_noContactsNotLoggedIn": "Log in or register to see your existing contacts and add new ones."
|
"share_noContactsNotLoggedIn": "Log in or register to see your existing contacts and add new ones.",
|
||||||
|
"contacts_mute": "Mute",
|
||||||
|
"contacts_unmute": "Unmute",
|
||||||
|
"contacts_manageMuted": "Manage muted",
|
||||||
|
"contacts_mutedUsers": "Muted accounts",
|
||||||
|
"contacts_muteInfo": "You will not receive any notifications or messages from muted users.<br>They will not know you have muted them. ",
|
||||||
|
"team_inviteLinkTitle": "Create a personalized invitation to this team",
|
||||||
|
"team_inviteLinkTempName": "Temporary name (visible in pending invitations list)",
|
||||||
|
"team_inviteLinkSetPassword": "Protect the link with a password (recommended)",
|
||||||
|
"team_inviteLinkNote": "Add a personal message",
|
||||||
|
"team_inviteLinkNoteMsg": "This message will be shown before the recipient decides whether to join this team.",
|
||||||
|
"team_inviteLinkLoading": "Generating your link",
|
||||||
|
"team_inviteLinkWarning": "The first person to access this link will be able to join this team and view its contents. Share it carefully.",
|
||||||
|
"team_inviteLinkErrorName": "Please add a name for the person you're inviting. They can change it later. ",
|
||||||
|
"team_inviteLinkCreate": "Create link",
|
||||||
|
"team_inviteLinkCopy": "Copy link",
|
||||||
|
"team_inviteFrom": "From:",
|
||||||
|
"team_inviteFromMsg": "{0} has invited you to join the team <b>{1}</b>",
|
||||||
|
"team_invitePleaseLogin": "Please log in or register to accept this invitation.",
|
||||||
|
"team_inviteEnterPassword": "Please enter the invitation password to continue.",
|
||||||
|
"team_invitePasswordLoading": "Decrypting invitation",
|
||||||
|
"team_inviteJoin": "Join team",
|
||||||
|
"team_inviteTitle": "Team invitation",
|
||||||
|
"team_inviteGetData": "Getting team data",
|
||||||
|
"team_cat_link": "Invitation Link",
|
||||||
|
"team_links": "Invitation Links",
|
||||||
|
"team_inviteInvalidLinkError": "This invitation link is not valid.",
|
||||||
|
"team_inviteLinkError": "There was an error while creating the link."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@import (reference) '../../customize/src/less2/include/framework.less';
|
@import (reference) '../../customize/src/less2/include/framework.less';
|
||||||
@import (reference) '../../customize/src/less2/include/messenger.less';
|
@import (reference) '../../customize/src/less2/include/messenger.less';
|
||||||
|
@import (reference) '../../customize/src/less2/include/avatar.less';
|
||||||
|
@import (reference) '../../customize/src/less2/include/buttons.less';
|
||||||
|
|
||||||
// body
|
// body
|
||||||
&.cp-app-contacts {
|
&.cp-app-contacts {
|
||||||
@@ -17,6 +19,22 @@
|
|||||||
display: flex; // We need this to remove a 3px border at the bottom of the toolbar
|
display: flex; // We need this to remove a 3px border at the bottom of the toolbar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cp-app-contacts-friends {
|
||||||
|
.buttons_main();
|
||||||
|
}
|
||||||
|
.cp-contacts-muted-table {
|
||||||
|
.avatar_main(50px);
|
||||||
|
.cp-contacts-muted-user {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
span:nth-child(2) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.messenger_main();
|
.messenger_main();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,22 @@
|
|||||||
);
|
);
|
||||||
.sidebar-layout_main();
|
.sidebar-layout_main();
|
||||||
|
|
||||||
|
@cp-profile-is-your-friend: #777;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
|
|
||||||
|
#cp-sidebarlayout-leftside {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
#cp-app-profile-header {
|
#cp-app-profile-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
#cp-app-profile-rightside {
|
#cp-app-profile-rightside {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#cp-app-profile-avatar {
|
#cp-app-profile-avatar {
|
||||||
@@ -64,16 +72,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
|
min-width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
margin: 5px;
|
margin: 5px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.cp-app-profile-resizer {
|
.cp-app-profile-resizer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
#cp-app-profile-displayname, #cp-app-profile-link {
|
#cp-app-profile-displayname, #cp-app-profile-link {
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -106,15 +113,30 @@
|
|||||||
}
|
}
|
||||||
.cp-app-profile-link-code {
|
.cp-app-profile-link-code {
|
||||||
display: none;
|
display: none;
|
||||||
|
min-width: 500px;
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
height: 48px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
& > button:empty {
|
& > button:empty {
|
||||||
margin-left: 25px;
|
margin-left: 25px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cp-app-profile-friend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: @cp-profile-is-your-friend;
|
||||||
|
.fa {
|
||||||
|
margin-right: 0.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
.cp-app-profile-friend-request {
|
.cp-app-profile-friend-request {
|
||||||
flex: 0;
|
flex: 0;
|
||||||
width: 400px;
|
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,12 +145,7 @@
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
.cp-app-profile-viewprofile-button {
|
.cp-app-profile-viewprofile-button {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 5px !important;
|
||||||
float: right;
|
|
||||||
margin-left: 5px;
|
|
||||||
&> span {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#cp-app-profile-description {
|
#cp-app-profile-description {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -166,13 +183,6 @@
|
|||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.cp-app-profile-description-edit {
|
|
||||||
& > button {
|
|
||||||
span {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#cp-app-profile-create {
|
#cp-app-profile-create {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -181,5 +191,12 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.cp-app-profile-mute-container {
|
||||||
|
margin-top: 5px;
|
||||||
|
p {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ define([
|
|||||||
}, Messages.profile_addLink);
|
}, Messages.profile_addLink);
|
||||||
APP.$linkEdit = $(button);
|
APP.$linkEdit = $(button);
|
||||||
$block.append(button);
|
$block.append(button);
|
||||||
var save = h('button.btn.btn-success', Messages.settings_save);
|
var save = h('button.btn.btn-primary', Messages.settings_save);
|
||||||
var text = h('input');
|
var text = h('input');
|
||||||
var code = h('div.cp-app-profile-link-code', [
|
var code = h('div.cp-app-profile-link-code', [
|
||||||
text,
|
text,
|
||||||
@@ -149,7 +149,7 @@ define([
|
|||||||
$block.append(div);
|
$block.append(div);
|
||||||
$(button).click(function () {
|
$(button).click(function () {
|
||||||
$(text).val(APP.$link.attr('href'));
|
$(text).val(APP.$link.attr('href'));
|
||||||
$(code).show();
|
$(code).css('display', 'flex');
|
||||||
APP.editor.refresh();
|
APP.editor.refresh();
|
||||||
$(button).hide();
|
$(button).hide();
|
||||||
});
|
});
|
||||||
@@ -178,9 +178,6 @@ define([
|
|||||||
|
|
||||||
var addFriendRequest = function ($container) {
|
var addFriendRequest = function ($container) {
|
||||||
if (!APP.readOnly || !APP.common.isLoggedIn()) { return; }
|
if (!APP.readOnly || !APP.common.isLoggedIn()) { return; }
|
||||||
APP.$friend = $('<button>', {
|
|
||||||
'class': 'btn btn-success cp-app-profile-friend-request',
|
|
||||||
});
|
|
||||||
APP.$friend = $(h('div.cp-app-profile-friend-container'));
|
APP.$friend = $(h('div.cp-app-profile-friend-container'));
|
||||||
$container.append(APP.$friend);
|
$container.append(APP.$friend);
|
||||||
};
|
};
|
||||||
@@ -195,42 +192,124 @@ define([
|
|||||||
|
|
||||||
APP.$friend.html('');
|
APP.$friend.html('');
|
||||||
|
|
||||||
|
var module = common.makeUniversal('messenger');
|
||||||
|
var name = Util.fixHTML(data.name) || Messages.anonymous;
|
||||||
|
|
||||||
var friends = common.getMetadataMgr().getPrivateData().friends;
|
var friends = common.getMetadataMgr().getPrivateData().friends;
|
||||||
|
// This is a friend: display the "friend" message and an "unfriend" button
|
||||||
if (friends[data.curvePublic]) {
|
if (friends[data.curvePublic]) {
|
||||||
APP.$friend.append(h('p.cp-app-profile-friend', Messages._getKey('profile_friend', [data.name || Messages.anonymous])));
|
// Add friend message
|
||||||
|
APP.$friend.append(h('p.cp-app-profile-friend', [
|
||||||
|
h('i.fa.fa-address-book'),
|
||||||
|
Messages._getKey('profile_friend', [name])
|
||||||
|
]));
|
||||||
|
if (!friends[data.curvePublic].notifications) { return; }
|
||||||
|
// Add unfriend button
|
||||||
|
var unfriendButton = h('button.btn.btn-primary.cp-app-profile-friend-request', [
|
||||||
|
h('i.fa.fa-user-times'),
|
||||||
|
Messages.contacts_remove
|
||||||
|
]);
|
||||||
|
$(unfriendButton).click(function () {
|
||||||
|
// Unfriend confirm
|
||||||
|
var content = h('div', [
|
||||||
|
UI.setHTML(h('p'), Messages._getKey('contacts_confirmRemove', [name]))
|
||||||
|
]);
|
||||||
|
UI.confirm(content, function (yes) {
|
||||||
|
if (!yes) { return; }
|
||||||
|
module.execCommand('REMOVE_FRIEND', data.curvePublic, function (e) {
|
||||||
|
if (e) { return void console.error(e); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).appendTo(APP.$friend);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var $button = $('<button>', {
|
var button = h('button.btn.btn-success.cp-app-profile-friend-request', [
|
||||||
'class': 'btn btn-success cp-app-profile-friend-request',
|
h('i.fa.fa-user-plus'),
|
||||||
}).appendTo(APP.$friend);
|
]);
|
||||||
|
var $button = $(button).appendTo(APP.$friend);
|
||||||
|
|
||||||
// If this curve has sent us a friend request, we should not be able to sent it to them
|
// If this curve has sent us a friend request, we should not be able to sent it to them
|
||||||
var friendRequests = common.getFriendRequests();
|
var friendRequests = common.getFriendRequests();
|
||||||
if (friendRequests[data.curvePublic]) {
|
if (friendRequests[data.curvePublic]) {
|
||||||
$button.html(Messages._getKey('friendRequest_received', [data.name || Messages.anonymous]))
|
$button.append(Messages._getKey('friendRequest_received', [name || Messages.anonymous]))
|
||||||
.click(function () {
|
.click(function () {
|
||||||
UIElements.displayFriendRequestModal(common, friendRequests[data.curvePublic]);
|
UIElements.displayFriendRequestModal(common, friendRequests[data.curvePublic]);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pending friend (we've sent a friend request)
|
||||||
var pendingFriends = APP.common.getPendingFriends(); // Friend requests sent
|
var pendingFriends = APP.common.getPendingFriends(); // Friend requests sent
|
||||||
if (pendingFriends[data.curvePublic]) {
|
if (pendingFriends[data.curvePublic]) {
|
||||||
$button.attr('disabled', 'disabled').text(Messages.profile_friendRequestSent);
|
$button.attr('disabled', 'disabled').append(Messages.profile_friendRequestSent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// This is not a friend yet: we can send a friend request
|
||||||
$button.text(Messages._getKey('userlist_addAsFriendTitle', [data.name || Messages.anonymous]))
|
$button.text(Messages._getKey('userlist_addAsFriendTitle', [data.name || Messages.anonymous]))
|
||||||
.click(function () {
|
.click(function () {
|
||||||
APP.common.sendFriendRequest({
|
APP.common.sendFriendRequest({
|
||||||
curvePublic: data.curvePublic,
|
curvePublic: data.curvePublic,
|
||||||
notifications: data.notifications
|
notifications: data.notifications
|
||||||
}, function () {
|
}, function () {
|
||||||
$button.attr('disabled', 'disabled').text(Messages.profile_friendRequestSent);
|
$button.attr('disabled', 'disabled').append(Messages.profile_friendRequestSent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var addMuteButton = function ($container) {
|
||||||
|
if (!APP.readOnly || !APP.common.isLoggedIn()) { return; }
|
||||||
|
APP.$mute = $(h('div.cp-app-profile-mute-container'));
|
||||||
|
$container.append(APP.$mute);
|
||||||
|
};
|
||||||
|
var refreshMute = function (data) {
|
||||||
|
if (!APP.$mute) { return; }
|
||||||
|
|
||||||
|
var me = common.getMetadataMgr().getUserData().curvePublic;
|
||||||
|
if (data.curvePublic === me) {
|
||||||
|
APP.$mute.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Add mute/unmute buttons
|
||||||
|
var $mute = APP.$mute;
|
||||||
|
var module = common.makeUniversal('messenger');
|
||||||
|
module.execCommand('GET_MUTED_USERS', null, function (muted) {
|
||||||
|
if (!muted || typeof(muted) !== "object") { return; }
|
||||||
|
$mute.html('');
|
||||||
|
var isMuted = muted[data.curvePublic];
|
||||||
|
if (isMuted) {
|
||||||
|
var unmuteButton = h('button.btn.btn-secondary.cp-app-profile-friend-request', [
|
||||||
|
h('i.fa.fa-bell'),
|
||||||
|
Messages.contacts_unmute || 'unmute'
|
||||||
|
]);
|
||||||
|
$(unmuteButton).click(function () {
|
||||||
|
module.execCommand('UNMUTE_USER', data.curvePublic, function (e) {
|
||||||
|
if (e) { console.error(e); return void UI.warn(Messages.error); }
|
||||||
|
refreshMute(data);
|
||||||
|
});
|
||||||
|
}).appendTo($mute);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var muteButton = h('button.btn.btn-danger-outline.cp-app-profile-friend-request', [
|
||||||
|
h('i.fa.fa-bell-slash'),
|
||||||
|
Messages.contacts_mute || 'mute'
|
||||||
|
]);
|
||||||
|
$(muteButton).click(function () {
|
||||||
|
module.execCommand('MUTE_USER', {
|
||||||
|
curvePublic: data.curvePublic,
|
||||||
|
name: Util.fixHTML(data.displayName || data.name),
|
||||||
|
avatar: data.avatar
|
||||||
|
}, function (e) {
|
||||||
|
if (e) { console.error(e); return void UI.warn(Messages.error); }
|
||||||
|
refreshMute(data);
|
||||||
|
});
|
||||||
|
}).appendTo($mute);
|
||||||
|
$(UI.setHTML(h('p'), Messages.contacts_muteInfo)).appendTo($mute);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
var displayAvatar = function (val) {
|
var displayAvatar = function (val) {
|
||||||
var sframeChan = common.getSframeChannel();
|
var sframeChan = common.getSframeChannel();
|
||||||
var $span = APP.$avatar;
|
var $span = APP.$avatar;
|
||||||
@@ -304,7 +383,7 @@ define([
|
|||||||
};
|
};
|
||||||
|
|
||||||
var addDescription = function ($container) {
|
var addDescription = function ($container) {
|
||||||
var $block = $('<div>', {id: DESCRIPTION_ID}).appendTo($container);
|
var $block = $('<div>', {id: DESCRIPTION_ID, class:'cp-sidebarlayout-element'}).appendTo($container);
|
||||||
|
|
||||||
APP.$description = $('<div>', {'class': 'cp-app-profile-description-rendered'}).appendTo($block);
|
APP.$description = $('<div>', {'class': 'cp-app-profile-description-rendered'}).appendTo($block);
|
||||||
APP.$descriptionEdit = $();
|
APP.$descriptionEdit = $();
|
||||||
@@ -361,6 +440,17 @@ define([
|
|||||||
var refreshDescription = function (data) {
|
var refreshDescription = function (data) {
|
||||||
var val = Marked(data.description || "");
|
var val = Marked(data.description || "");
|
||||||
APP.$description.html(val);
|
APP.$description.html(val);
|
||||||
|
APP.$description.off('click');
|
||||||
|
APP.$description.click(function (e) {
|
||||||
|
if (!e.target) { return; }
|
||||||
|
var $t = $(e.target);
|
||||||
|
if ($t.is('a') || $t.parents('a').length) {
|
||||||
|
e.preventDefault();
|
||||||
|
var $a = $t.is('a') ? $t : $t.parents('a').first();
|
||||||
|
var href = $a.attr('href');
|
||||||
|
common.openUnsafeURL(href);
|
||||||
|
}
|
||||||
|
});
|
||||||
APP.$descriptionEdit.find('span').text(val === "" ? Messages.profile_addDescription : Messages.profile_editDescription);
|
APP.$descriptionEdit.find('span').text(val === "" ? Messages.profile_addDescription : Messages.profile_editDescription);
|
||||||
if (!APP.editor) { return; }
|
if (!APP.editor) { return; }
|
||||||
APP.editor.setValue(data.description || "");
|
APP.editor.setValue(data.description || "");
|
||||||
@@ -379,14 +469,15 @@ define([
|
|||||||
APP.$container.find('#'+CREATE_ID).remove();
|
APP.$container.find('#'+CREATE_ID).remove();
|
||||||
|
|
||||||
if (!APP.initialized) {
|
if (!APP.initialized) {
|
||||||
var $header = $('<div>', {id: HEADER_ID}).appendTo(APP.$rightside);
|
var $header = $('<div>', {id: HEADER_ID, class:'cp-sidebarlayout-element'}).appendTo(APP.$rightside);
|
||||||
addAvatar($header);
|
addAvatar($header);
|
||||||
var $rightside = $('<div>', {id: HEADER_RIGHT_ID}).appendTo($header);
|
var $rightside = $('<div>', {id: HEADER_RIGHT_ID}).appendTo($header);
|
||||||
addDisplayName($rightside);
|
addDisplayName($rightside);
|
||||||
addLink($rightside);
|
addLink($rightside);
|
||||||
addFriendRequest($rightside);
|
addFriendRequest($rightside);
|
||||||
|
addMuteButton($rightside);
|
||||||
addDescription(APP.$rightside);
|
addDescription(APP.$rightside);
|
||||||
addViewButton(APP.$rightside);
|
addViewButton($rightside);
|
||||||
APP.initialized = true;
|
APP.initialized = true;
|
||||||
createLeftside();
|
createLeftside();
|
||||||
}
|
}
|
||||||
@@ -398,6 +489,7 @@ define([
|
|||||||
refreshLink(data);
|
refreshLink(data);
|
||||||
refreshDescription(data);
|
refreshDescription(data);
|
||||||
refreshFriendRequest(data);
|
refreshFriendRequest(data);
|
||||||
|
refreshMute(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
var createToolbar = function () {
|
var createToolbar = function () {
|
||||||
|
|||||||
@@ -184,12 +184,12 @@
|
|||||||
&:checked {
|
&:checked {
|
||||||
& + label {
|
& + label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background-color: lighten(@colortheme_loading-bg, 20%);
|
background-color: @colortheme_logo-2;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
border: 1px solid #c1158e;
|
border: 1px solid @colortheme_logo-2;
|
||||||
color: @colortheme_loading-color;
|
color: @colortheme_loading-color;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: lighten(@colortheme_loading-bg, 20%);
|
background-color: @colortheme_logo-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,10 +203,11 @@
|
|||||||
height: 50px;
|
height: 50px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
border: 1px solid black;
|
color: @colortheme_logo-2;
|
||||||
|
border: 1px solid @colortheme_logo-2;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: lighten(@colortheme_loading-bg, 10%);
|
background-color: lighten(@colortheme_logo-2, 35%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fa {
|
.fa {
|
||||||
@@ -235,6 +236,7 @@
|
|||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
width: 70px;
|
width: 70px;
|
||||||
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@import (reference) '../../customize/src/less2/include/messenger.less';
|
@import (reference) '../../customize/src/less2/include/messenger.less';
|
||||||
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
|
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
|
||||||
@import (reference) "../../customize/src/less2/include/tools.less";
|
@import (reference) "../../customize/src/less2/include/tools.less";
|
||||||
|
@import (reference) "../../customize/src/less2/include/colortheme.less";
|
||||||
|
|
||||||
|
|
||||||
&.cp-app-team {
|
&.cp-app-team {
|
||||||
@@ -138,7 +139,11 @@
|
|||||||
}
|
}
|
||||||
.cp-team-roster {
|
.cp-team-roster {
|
||||||
.avatar_main(50px);
|
.avatar_main(50px);
|
||||||
|
.cp-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.cp-app-team-roster-header {
|
.cp-app-team-roster-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
button:not(:last-child) {
|
button:not(:last-child) {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
@@ -188,6 +193,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.cp-team-link {
|
||||||
|
max-width: 550px;
|
||||||
|
.cp-teams-invite-from {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
.cp-teams-invite-from-author {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 10px;
|
||||||
|
.cp-teams-invite-from-avatar {
|
||||||
|
.avatar_main(25px);
|
||||||
|
margin: 0px 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cp-teams-invite-message {
|
||||||
|
background-color: fade(@colortheme_logo-2, 25%);
|
||||||
|
color: @cryptpad_text_col;
|
||||||
|
resize: none;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.cp-teams-invite-password {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
i.fa.fa-spin.fa-spinner {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#cp-teams-roster-dialog {
|
#cp-teams-roster-dialog {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ define([
|
|||||||
'/common/hyperscript.js',
|
'/common/hyperscript.js',
|
||||||
'/customize/application_config.js',
|
'/customize/application_config.js',
|
||||||
'/common/messenger-ui.js',
|
'/common/messenger-ui.js',
|
||||||
|
'/common/invitation.js',
|
||||||
'/customize/messages.js',
|
'/customize/messages.js',
|
||||||
|
|
||||||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
|
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
|
||||||
@@ -37,6 +38,7 @@ define([
|
|||||||
h,
|
h,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
MessengerUI,
|
MessengerUI,
|
||||||
|
InviteInner,
|
||||||
Messages)
|
Messages)
|
||||||
{
|
{
|
||||||
var APP = {};
|
var APP = {};
|
||||||
@@ -137,6 +139,9 @@ define([
|
|||||||
'general': [
|
'general': [
|
||||||
'cp-team-info',
|
'cp-team-info',
|
||||||
],
|
],
|
||||||
|
'link': [
|
||||||
|
'cp-team-link',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
var teamCategories = {
|
var teamCategories = {
|
||||||
'back': {
|
'back': {
|
||||||
@@ -179,8 +184,10 @@ define([
|
|||||||
var $categories = $('<div>', {'class': 'cp-sidebarlayout-categories'})
|
var $categories = $('<div>', {'class': 'cp-sidebarlayout-categories'})
|
||||||
.appendTo(APP.$leftside);
|
.appendTo(APP.$leftside);
|
||||||
|
|
||||||
|
var hash = common.getMetadataMgr().getPrivateData().teamInviteHash && mainCategories.link;
|
||||||
|
|
||||||
var categories = team ? teamCategories : mainCategories;
|
var categories = team ? teamCategories : mainCategories;
|
||||||
var active = team ? 'drive' : 'list';
|
var active = team ? 'drive' : (hash ? 'link' : 'list');
|
||||||
|
|
||||||
if (team && APP.team) {
|
if (team && APP.team) {
|
||||||
var $category = $('<div>', {'class': 'cp-sidebarlayout-category cp-team-cat-header'}).appendTo($categories);
|
var $category = $('<div>', {'class': 'cp-sidebarlayout-category cp-team-cat-header'}).appendTo($categories);
|
||||||
@@ -210,6 +217,7 @@ define([
|
|||||||
if (key === 'chat') { $category.append($('<span>', {'class': 'fa fa-comments'})); }
|
if (key === 'chat') { $category.append($('<span>', {'class': 'fa fa-comments'})); }
|
||||||
if (key === 'drive') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); }
|
if (key === 'drive') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); }
|
||||||
if (key === 'admin') { $category.append($('<span>', {'class': 'fa fa-cogs'})); }
|
if (key === 'admin') { $category.append($('<span>', {'class': 'fa fa-cogs'})); }
|
||||||
|
if (key === 'link') { $category.append($('<span>', {'class': 'fa fa-envelope'})); }
|
||||||
|
|
||||||
if (key === active) {
|
if (key === active) {
|
||||||
$category.addClass('cp-leftside-active');
|
$category.addClass('cp-leftside-active');
|
||||||
@@ -413,6 +421,7 @@ define([
|
|||||||
refreshList(common, cb);
|
refreshList(common, cb);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var refreshLink = function () {}; // placeholder
|
||||||
var refreshCreate = function (common, cb) {
|
var refreshCreate = function (common, cb) {
|
||||||
var metadataMgr = common.getMetadataMgr();
|
var metadataMgr = common.getMetadataMgr();
|
||||||
var privateData = metadataMgr.getPrivateData();
|
var privateData = metadataMgr.getPrivateData();
|
||||||
@@ -428,7 +437,7 @@ define([
|
|||||||
}, isOwner ? Messages.team_maxOwner : Messages._getKey('team_maxTeams', [MAX_TEAMS_SLOTS]));
|
}, isOwner ? Messages.team_maxOwner : Messages._getKey('team_maxTeams', [MAX_TEAMS_SLOTS]));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Object.keys(privateData.teams || {}).length >= 3 || isOwner) {
|
if (Object.keys(privateData.teams || {}).length >= Constants.MAX_TEAMS_SLOTS || isOwner) {
|
||||||
content.push(getWarningBox());
|
content.push(getWarningBox());
|
||||||
return void cb(content);
|
return void cb(content);
|
||||||
}
|
}
|
||||||
@@ -471,6 +480,12 @@ define([
|
|||||||
$spinner.hide();
|
$spinner.hide();
|
||||||
$('div.cp-team-cat-list').click();
|
$('div.cp-team-cat-list').click();
|
||||||
});
|
});
|
||||||
|
var $divLink = $('div.cp-team-link').empty();
|
||||||
|
if ($divLink.length) {
|
||||||
|
refreshLink(common, function (content) {
|
||||||
|
$divLink.append(content);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cb(content);
|
cb(content);
|
||||||
@@ -479,8 +494,9 @@ define([
|
|||||||
refreshCreate(common, cb);
|
refreshCreate(common, cb);
|
||||||
});
|
});
|
||||||
|
|
||||||
makeBlock('drive', function (common, cb) {
|
makeBlock('drive', function (common, cb, $div) {
|
||||||
$('div.cp-team-drive').empty();
|
$('div.cp-team-drive').empty();
|
||||||
|
$div.removeClass('cp-sidebarlayout-element'); // Don't apply buttons and input styles from sidebarlayout
|
||||||
var content = [
|
var content = [
|
||||||
h('div.cp-app-drive-container', {tabindex:0}, [
|
h('div.cp-app-drive-container', {tabindex:0}, [
|
||||||
h('div#cp-app-drive-tree'),
|
h('div#cp-app-drive-tree'),
|
||||||
@@ -713,9 +729,7 @@ define([
|
|||||||
actions,
|
actions,
|
||||||
status,
|
status,
|
||||||
];
|
];
|
||||||
var div = h('div.cp-team-roster-member', {
|
var div = h('div.cp-team-roster-member', content);
|
||||||
title: data.displayName
|
|
||||||
}, content);
|
|
||||||
if (data.profile) {
|
if (data.profile) {
|
||||||
$(div).dblclick(function (e) {
|
$(div).dblclick(function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -732,34 +746,48 @@ define([
|
|||||||
var me = roster[userData.curvePublic] || {};
|
var me = roster[userData.curvePublic] || {};
|
||||||
var owner = Object.keys(roster).filter(function (k) {
|
var owner = Object.keys(roster).filter(function (k) {
|
||||||
if (roster[k].pending) { return; }
|
if (roster[k].pending) { return; }
|
||||||
|
roster[k].curvePublic = k;
|
||||||
return roster[k].role === "OWNER" || roster[k].pendingOwner;
|
return roster[k].role === "OWNER" || roster[k].pendingOwner;
|
||||||
}).map(function (k) {
|
}).map(function (k) {
|
||||||
return makeMember(common, roster[k], me, roster);
|
return makeMember(common, roster[k], me, roster);
|
||||||
});
|
});
|
||||||
var admins = Object.keys(roster).filter(function (k) {
|
var admins = Object.keys(roster).filter(function (k) {
|
||||||
if (roster[k].pending) { return; }
|
if (roster[k].pending) { return; }
|
||||||
|
roster[k].curvePublic = k;
|
||||||
return roster[k].role === "ADMIN";
|
return roster[k].role === "ADMIN";
|
||||||
}).map(function (k) {
|
}).map(function (k) {
|
||||||
return makeMember(common, roster[k], me);
|
return makeMember(common, roster[k], me);
|
||||||
});
|
});
|
||||||
var members = Object.keys(roster).filter(function (k) {
|
var members = Object.keys(roster).filter(function (k) {
|
||||||
if (roster[k].pending) { return; }
|
if (roster[k].pending) { return; }
|
||||||
|
roster[k].curvePublic = k;
|
||||||
return roster[k].role === "MEMBER" || !roster[k].role;
|
return roster[k].role === "MEMBER" || !roster[k].role;
|
||||||
}).map(function (k) {
|
}).map(function (k) {
|
||||||
return makeMember(common, roster[k], me);
|
return makeMember(common, roster[k], me);
|
||||||
});
|
});
|
||||||
var viewers = Object.keys(roster).filter(function (k) {
|
var viewers = Object.keys(roster).filter(function (k) {
|
||||||
if (roster[k].pending) { return; }
|
if (roster[k].pending) { return; }
|
||||||
|
roster[k].curvePublic = k;
|
||||||
return roster[k].role === "VIEWER";
|
return roster[k].role === "VIEWER";
|
||||||
}).map(function (k) {
|
}).map(function (k) {
|
||||||
return makeMember(common, roster[k], me);
|
return makeMember(common, roster[k], me);
|
||||||
});
|
});
|
||||||
var pending = Object.keys(roster).filter(function (k) {
|
var pending = Object.keys(roster).filter(function (k) {
|
||||||
if (!roster[k].pending) { return; }
|
if (!roster[k].pending) { return; }
|
||||||
|
if (roster[k].inviteChannel) { return; }
|
||||||
|
roster[k].curvePublic = k;
|
||||||
return roster[k].role === "MEMBER" || roster[k].role === "VIEWER" || !roster[k].role;
|
return roster[k].role === "MEMBER" || roster[k].role === "VIEWER" || !roster[k].role;
|
||||||
}).map(function (k) {
|
}).map(function (k) {
|
||||||
return makeMember(common, roster[k], me);
|
return makeMember(common, roster[k], me);
|
||||||
});
|
});
|
||||||
|
var links = Object.keys(roster).filter(function (k) {
|
||||||
|
if (!roster[k].pending) { return; }
|
||||||
|
if (!roster[k].inviteChannel) { return; }
|
||||||
|
roster[k].curvePublic = k;
|
||||||
|
return roster[k].role === "VIEWER" || !roster[k].role;
|
||||||
|
}).map(function (k) {
|
||||||
|
return makeMember(common, roster[k], me);
|
||||||
|
});
|
||||||
|
|
||||||
var header = h('div.cp-app-team-roster-header');
|
var header = h('div.cp-app-team-roster-header');
|
||||||
var $header = $(header);
|
var $header = $(header);
|
||||||
@@ -813,6 +841,7 @@ define([
|
|||||||
$header.append(table);
|
$header.append(table);
|
||||||
|
|
||||||
var noPending = pending.length ? '' : '.cp-hidden';
|
var noPending = pending.length ? '' : '.cp-hidden';
|
||||||
|
var noLinks = links.length ? '' : '.cp-hidden';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
header,
|
header,
|
||||||
@@ -825,7 +854,9 @@ define([
|
|||||||
h('h3', Messages.team_viewers || 'VIEWERS'),
|
h('h3', Messages.team_viewers || 'VIEWERS'),
|
||||||
h('div', viewers),
|
h('div', viewers),
|
||||||
h('h3'+noPending, Messages.team_pending),
|
h('h3'+noPending, Messages.team_pending),
|
||||||
h('div'+noPending, pending)
|
h('div'+noPending, pending),
|
||||||
|
h('h3'+noLinks, Messages.team_links),
|
||||||
|
h('div'+noLinks, links)
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
makeBlock('roster', function (common, cb) {
|
makeBlock('roster', function (common, cb) {
|
||||||
@@ -1012,6 +1043,206 @@ define([
|
|||||||
]);
|
]);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
var displayUser = function (common, data) {
|
||||||
|
var avatar = h('span.cp-teams-invite-from-avatar.cp-avatar');
|
||||||
|
UIElements.displayAvatar(common, $(avatar), data.avatar, data.displayName);
|
||||||
|
return h('div.cp-teams-invite-from-author', [
|
||||||
|
avatar,
|
||||||
|
h('span.cp-teams-invite-from-name', data.displayName)
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshLink = function (common, cb, wrongPassword) {
|
||||||
|
if (!mainCategories.link) { return; }
|
||||||
|
var privateData = common.getMetadataMgr().getPrivateData();
|
||||||
|
var hash = privateData.teamInviteHash;
|
||||||
|
var hashData = Hash.parseTypeHash('invite', hash);
|
||||||
|
var password = hashData.password;
|
||||||
|
var seeds = InviteInner.deriveSeeds(hashData.key);
|
||||||
|
|
||||||
|
if (Object.keys(privateData.teams || {}).length >= Constants.MAX_TEAMS_SLOTS) {
|
||||||
|
return void cb([
|
||||||
|
h('div.alert.alert-danger', {
|
||||||
|
role: 'alert'
|
||||||
|
}, Messages._getKey('team_maxTeams', [Constants.MAX_TEAMS_SLOTS]))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var div = h('div', [
|
||||||
|
h('i.fa.fa-spin.fa-spinner')
|
||||||
|
]);
|
||||||
|
var $div = $(div);
|
||||||
|
var errorBlock;
|
||||||
|
var c = [
|
||||||
|
h('h2', Messages.team_inviteTitle),
|
||||||
|
errorBlock = h('div.alert.alert-danger',
|
||||||
|
wrongPassword ? undefined : {style: 'display: none;'},
|
||||||
|
wrongPassword ? Messages.drive_sfPasswordError : undefined),
|
||||||
|
div
|
||||||
|
];
|
||||||
|
// "cb" will put the content into the UI.
|
||||||
|
// We're displaying a spinner while we're cryptgetting the preview content
|
||||||
|
cb(c);
|
||||||
|
|
||||||
|
var declineButton = h('button.btn.btn-danger', {
|
||||||
|
style: 'display: none;'
|
||||||
|
}, Messages.friendRequest_decline);
|
||||||
|
var acceptButton = h('button.btn.btn-primary', Messages.team_inviteJoin);
|
||||||
|
var inviteDiv = h('div', [
|
||||||
|
h('nav', [
|
||||||
|
declineButton,
|
||||||
|
acceptButton
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
var $inviteDiv = $(inviteDiv);
|
||||||
|
|
||||||
|
$(declineButton).click(function() {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var process = function (pw) {
|
||||||
|
$inviteDiv.empty();
|
||||||
|
var bytes64;
|
||||||
|
|
||||||
|
|
||||||
|
var spinnerText;
|
||||||
|
var $spinner;
|
||||||
|
nThen(function (waitFor) {
|
||||||
|
$inviteDiv.append(h('div', [
|
||||||
|
h('i.fa.fa-spin.fa-spinner'),
|
||||||
|
spinnerText = h('span', Messages.team_invitePasswordLoading || 'Scrypt...')
|
||||||
|
]));
|
||||||
|
$spinner = $(spinnerText);
|
||||||
|
setTimeout(waitFor(), 150);
|
||||||
|
}).nThen(function (waitFor) {
|
||||||
|
var salt = InviteInner.deriveSalt(pw, AppConfig.loginSalt);
|
||||||
|
InviteInner.deriveBytes(seeds.scrypt, salt, waitFor(function (bytes) {
|
||||||
|
bytes64 = bytes;
|
||||||
|
}));
|
||||||
|
}).nThen(function (waitFor) {
|
||||||
|
$spinner.text(Messages.team_inviteGetData);
|
||||||
|
APP.module.execCommand('ACCEPT_LINK_INVITATION', {
|
||||||
|
bytes64: bytes64,
|
||||||
|
hash: hash,
|
||||||
|
password: pw,
|
||||||
|
}, waitFor(function (obj) {
|
||||||
|
if (obj && obj.error) {
|
||||||
|
console.error(obj.error);
|
||||||
|
// Wrong password or other error...
|
||||||
|
waitFor.abort();
|
||||||
|
if (obj.error === 'INVALID_INVITE_CONTENT') {
|
||||||
|
// Wrong password...
|
||||||
|
var $divLink = $('div.cp-team-link').empty();
|
||||||
|
if ($divLink.length) {
|
||||||
|
refreshLink(common, function (content) {
|
||||||
|
$divLink.append(content);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$(errorBlock).text(Messages.team_inviteInvalidLinkError).show();
|
||||||
|
$(div).empty();
|
||||||
|
$inviteDiv.empty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// No error: join successful!
|
||||||
|
var $div = $('div.cp-team-list').empty();
|
||||||
|
refreshList(common, function (content) {
|
||||||
|
$div.append(content);
|
||||||
|
$('div.cp-team-cat-list').click();
|
||||||
|
var $divLink = $('div.cp-team-link').empty();
|
||||||
|
if ($divLink.length) {
|
||||||
|
$divLink.remove();
|
||||||
|
$('div.cp-team-cat-link').remove();
|
||||||
|
delete mainCategories.link;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var $divCreate = $('div.cp-team-create');
|
||||||
|
if ($divCreate.length) {
|
||||||
|
refreshCreate(common, function (content) {
|
||||||
|
$divCreate.empty().append(content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
nThen(function (waitFor) {
|
||||||
|
// Get preview content.
|
||||||
|
var sframeChan = common.getSframeChannel();
|
||||||
|
sframeChan.query('Q_ANON_GET_PREVIEW_CONTENT', { seeds: seeds }, waitFor(function (err, json) {
|
||||||
|
if (json && (json.error || !Object.keys(json).length)) {
|
||||||
|
$(errorBlock).text(Messages.team_inviteInvalidLinkError).show();
|
||||||
|
waitFor.abort();
|
||||||
|
$div.empty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$div.empty();
|
||||||
|
$div.append(h('div.cp-teams-invite-from', [
|
||||||
|
Messages.team_inviteFrom || 'From:',
|
||||||
|
displayUser(common, json.author)
|
||||||
|
]));
|
||||||
|
$div.append(UI.setHTML(h('p.cp-teams-invite-to'),
|
||||||
|
Messages._getKey('team_inviteFromMsg',
|
||||||
|
[Util.fixHTML(json.author.displayName),
|
||||||
|
Util.fixHTML(json.teamName)])));
|
||||||
|
if (json.message) {
|
||||||
|
$div.append(h('div.cp-teams-invite-message', json.message));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}).nThen(function (waitFor) {
|
||||||
|
// If you're logged in, move on to the next nThen
|
||||||
|
if (driveAPP.loggedIn) { return; }
|
||||||
|
|
||||||
|
// If you're not logged in, display the login buttons
|
||||||
|
var anonLogin, anonRegister;
|
||||||
|
$div.append(h('p', Messages.team_invitePleaseLogin));
|
||||||
|
$div.append(h('div', [
|
||||||
|
anonLogin = h('button.btn.btn-primary', Messages.login_login),
|
||||||
|
anonRegister = h('button.btn.btn-secondary', Messages.login_register),
|
||||||
|
]));
|
||||||
|
$(anonLogin).click(function () {
|
||||||
|
common.setLoginRedirect(function () {
|
||||||
|
common.gotoURL('/login/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$(anonRegister).click(function () {
|
||||||
|
common.setLoginRedirect(function () {
|
||||||
|
common.gotoURL('/register/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
waitFor.abort();
|
||||||
|
}).nThen(function () {
|
||||||
|
$div.append($inviteDiv);
|
||||||
|
}).nThen(function (waitFor) {
|
||||||
|
// If there is no password, move on to the next block
|
||||||
|
if (!password) { return; }
|
||||||
|
|
||||||
|
// If there is a password, display the password prompt
|
||||||
|
var pwInput = UI.passwordInput();
|
||||||
|
$(acceptButton).click(function () {
|
||||||
|
var val = $(pwInput).find('input').val();
|
||||||
|
if (!val) { return; }
|
||||||
|
process(val);
|
||||||
|
});
|
||||||
|
$inviteDiv.prepend(h('div.cp-teams-invite-password', [
|
||||||
|
h('p', Messages.team_inviteEnterPassword),
|
||||||
|
pwInput
|
||||||
|
]));
|
||||||
|
waitFor.abort();
|
||||||
|
}).nThen(function () {
|
||||||
|
// No password, display the invitation proposal
|
||||||
|
$(acceptButton).click(function () {
|
||||||
|
process('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
makeBlock('link', function (common, cb) {
|
||||||
|
refreshLink(common, cb);
|
||||||
|
});
|
||||||
|
|
||||||
var redrawTeam = function (common) {
|
var redrawTeam = function (common) {
|
||||||
if (!APP.team) { return; }
|
if (!APP.team) { return; }
|
||||||
var teamId = APP.team;
|
var teamId = APP.team;
|
||||||
@@ -1053,7 +1284,7 @@ define([
|
|||||||
readOnly = driveAPP.readOnly = metadataMgr.getPrivateData().readOnly;
|
readOnly = driveAPP.readOnly = metadataMgr.getPrivateData().readOnly;
|
||||||
|
|
||||||
driveAPP.loggedIn = common.isLoggedIn();
|
driveAPP.loggedIn = common.isLoggedIn();
|
||||||
if (!driveAPP.loggedIn) { throw new Error('NOT_LOGGED_IN'); }
|
//if (!driveAPP.loggedIn) { throw new Error('NOT_LOGGED_IN'); }
|
||||||
|
|
||||||
common.setTabTitle(Messages.type.teams);
|
common.setTabTitle(Messages.type.teams);
|
||||||
|
|
||||||
@@ -1103,6 +1334,22 @@ define([
|
|||||||
onEvent: onEvent
|
onEvent: onEvent
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var hash = privateData.teamInviteHash;
|
||||||
|
if (!hash && !driveAPP.loggedIn) {
|
||||||
|
UI.alert(Messages.mustLogin, function () {
|
||||||
|
common.setLoginRedirect(function () {
|
||||||
|
common.gotoURL('/login/');
|
||||||
|
});
|
||||||
|
}, {forefront: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hash) {
|
||||||
|
delete mainCategories.link;
|
||||||
|
} else if (!driveAPP.loggedIn) {
|
||||||
|
delete mainCategories.list;
|
||||||
|
delete mainCategories.create;
|
||||||
|
}
|
||||||
|
|
||||||
$('body').css('display', '');
|
$('body').css('display', '');
|
||||||
loadMain(common);
|
loadMain(common);
|
||||||
|
|
||||||
@@ -1113,6 +1360,12 @@ define([
|
|||||||
$div.empty().append(content);
|
$div.empty().append(content);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
var $divLink = $('div.cp-team-link').empty();
|
||||||
|
if ($divLink.length) {
|
||||||
|
refreshLink(common, function (content) {
|
||||||
|
$divLink.append(content);
|
||||||
|
});
|
||||||
|
}
|
||||||
var $divCreate = $('div.cp-team-create');
|
var $divCreate = $('div.cp-team-create');
|
||||||
if ($divCreate.length) {
|
if ($divCreate.length) {
|
||||||
refreshCreate(common, function (content) {
|
refreshCreate(common, function (content) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ define([
|
|||||||
window.addEventListener('message', onMsg);
|
window.addEventListener('message', onMsg);
|
||||||
}).nThen(function (/*waitFor*/) {
|
}).nThen(function (/*waitFor*/) {
|
||||||
var teamId;
|
var teamId;
|
||||||
|
var hash = window.location.hash.slice(1);
|
||||||
var addRpc = function (sframeChan, Cryptpad) {
|
var addRpc = function (sframeChan, Cryptpad) {
|
||||||
sframeChan.on('Q_SET_TEAM', function (data, cb) {
|
sframeChan.on('Q_SET_TEAM', function (data, cb) {
|
||||||
teamId = data;
|
teamId = data;
|
||||||
@@ -86,11 +87,23 @@ define([
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
var getSecrets = function (Cryptpad, Utils, cb) {
|
||||||
|
var Hash = Utils.Hash;
|
||||||
|
var hash = Hash.createRandomHash('profile');
|
||||||
|
var secret = Hash.getSecrets('team', hash);
|
||||||
|
cb(null, secret);
|
||||||
|
};
|
||||||
|
var addData = function (meta) {
|
||||||
|
if (!hash) { return; }
|
||||||
|
meta.teamInviteHash = hash;
|
||||||
|
};
|
||||||
SFCommonO.start({
|
SFCommonO.start({
|
||||||
|
getSecrets: getSecrets,
|
||||||
noHash: true,
|
noHash: true,
|
||||||
noRealtime: true,
|
noRealtime: true,
|
||||||
//driveEvents: true,
|
//driveEvents: true,
|
||||||
addRpc: addRpc,
|
addRpc: addRpc,
|
||||||
|
addData: addData,
|
||||||
isDrive: true, // Used for history...
|
isDrive: true, // Used for history...
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user