Merge branch 'staging' into oo2

pull/1/head
ansuz 6 years ago
commit b181d72727

@ -2,6 +2,7 @@ node_modules/
www/bower_components/ www/bower_components/
www/common/pdfjs/ www/common/pdfjs/
www/common/tippy/ www/common/tippy/
www/common/highlight/
www/common/jquery-ui/ www/common/jquery-ui/
www/common/onlyoffice/* www/common/onlyoffice/*
@ -18,6 +19,7 @@ www/pad/mediatag-plugin-dialog.js
www/pad/disable-base64.js www/pad/disable-base64.js
www/kanban/jkanban.js www/kanban/jkanban.js
www/kanban/jscolor.js
www/common/media-tag-nacl.min.js www/common/media-tag-nacl.min.js

@ -1,3 +1,85 @@
# Ibis release (v2.8.0)
## Goals
We've been making use of some hidden features for a while, to make sure that they were safe to deploy.
This release, we worked on making _contextual chat_ and _shared folders_ available to everyone.
## Update notes
* run `bower update` to download an updated version of _marked.js_
### Features
* Our kanban application now features a much more consistent and flexible colorpicker, thanks to @MTRNord (https://github.com/MTRNord)
* File upload dialogs now allow you to upload multiple files at once
* Updated German translations thanks to [b3yond](https://github.com/b3yond/)
* An explicit pad storage policy to better suit different privacy constraints
* _import local pads_ at login time is no longer default
* An embedded chat room in every pad, so you can work alongside your fellow editors more easily
* Promotion of our [crowdfunding campaign](https://opencollective.com/cryptpad), including a button on the home page, and a one-time dialog for users
### Bug fixes
* Updating our markdown library resolved an issue which incorrectly rendered links containing parentheses.
* We discovered an issue logging in with _very old_ credentials which were initialized without a public key. We now regenerate your keyring if you do not have public keys stored in association with your account.
* We found another bug in our login process; under certain conditions the terminating function could be called more than once.
# Hedgehog release (v2.7.0)
## Goals
This release overlapped with the publication and presentation of a paper written about CryptPad's architecture.
As such, we didn't plan for any very ambitious new features, and instead focused on bug fixes and some new workflows.
## Update notes
This is a fairly simple release. Just download the latest commits and update your cache-busting string.
### Features
* In order to address some privacy concerns, we've changed CryptPad such that pads are not immediately stored in your CryptDrive as soon as you open them. Instead, users are presented with a prompt in the bottom-right corner which asks them whether they'd like to store it manually. Alternatively, you can use your settings page to revert to the old automatic behaviour, or choose not to store, and to never be asked.
* It was brought to our attention that it was possible to upload base64-encoded images in the rich text editor. These images had a negative performance impact on such pads. From now on, if these images are detected in a pad, users are prompted to run a migration to convert them to uploaded (and encrypted) files.
* We've added a progress bar which is displayed while you are loading a pad, as we found that it was not very clear whether large pads were loading, or if they had become unresponsive due to a bug.
* We've added an option to allow users to right-click uploaded files wherever they appear, and to store that file in their CryptDrive.
* We've improved the dialog which is used to modify the properties of encrypted media embedded within rich text pads.
### Bug fixes
* Due to a particularly disastrous bug in Chrome 68 which was unfortunately beyond our power to fix, we've added a warning for anyone affected by that bug to let them know the cause.
* We've increased the module loading timeout value used by requirejs in our sharedWorker implementation to match the value used by the rest of CryptPad.
# Gibbon release (v2.6.0)
## Goals
For this release we focused on deploying two very large changes in CryptPad.
For one, we'd worked on a large refactoring of the system we use to compile CSS from LESS, so as to make it more efficient.
Secondly, we reworked the architecture we use for implementing the CryptDrive functionality, so as to integrate support for shared folders.
## Update notes
To test the _shared folders_ functionality, users can run the following command in their browser console:
`localStorage.CryptPad_SF = "1";`
Alternatively, if the instance administrator would like to enable shared folders for all users, they can do so via their `/customize/application_config.js` file, by adding the following line:
`config.disableSharedFolders = true;`
### Features
* As mentioned in the _goals_ for this release, we've merged in the work done to drastically improve performance when compiling styles. The system features documentation for anyone interested in understanding how it works.
* We've refactored the APIs used to interact with your CryptDrive, implementing a single interface with which applications can interact, which then manages any number of sub-objects each representing a shared folder. Shared folders are still disabled by default. See the _Update notes_ section for more information.
* The home page now features the same footer which has been displayed on all other information pages until now.
* We've added a slightly nicer spinner icon on loading pages.
* We've created a custom font _cp-tools_ for our custom-designed icons
### Bug fixes
* We've accepted a pull request implementing serverside support for moving files across different drives, for system administrators hosting CryptPad on systems which segregate folders on different partitions.
* We've addressed a report of an edge case in CryptPad's user password change logic which could cause users to delete their accounts.
# Fossa release (v2.5.0) # Fossa release (v2.5.0)
## Goals ## Goals

@ -24,7 +24,7 @@
"ckeditor": "4.7.3", "ckeditor": "4.7.3",
"codemirror": "^5.19.0", "codemirror": "^5.19.0",
"requirejs": "2.3.5", "requirejs": "2.3.5",
"marked": "0.3.5", "marked": "0.5.0",
"rangy": "rangy-release#~1.3.0", "rangy": "rangy-release#~1.3.0",
"json.sortify": "~2.1.0", "json.sortify": "~2.1.0",
"secure-fabric.js": "secure-v1.7.9", "secure-fabric.js": "secure-v1.7.9",

@ -191,7 +191,10 @@ define([
// load the user's object using the legacy credentials // load the user's object using the legacy credentials
loadUserObject(opt, waitFor(function (err, rt) { loadUserObject(opt, waitFor(function (err, rt) {
if (err) { return void cb(err); } if (err) {
waitFor.abort();
return void cb(err);
}
// if a proxy is marked as deprecated, it is because someone had a non-owned drive // if a proxy is marked as deprecated, it is because someone had a non-owned drive
// but changed their password, and couldn't delete their old data. // but changed their password, and couldn't delete their old data.
@ -199,6 +202,7 @@ define([
// allow them to proceed. In time, their old drive should get deleted, since // allow them to proceed. In time, their old drive should get deleted, since
// it will should be pinned by anyone's drive. // it will should be pinned by anyone's drive.
if (rt.proxy[Constants.deprecatedKey]) { if (rt.proxy[Constants.deprecatedKey]) {
waitFor.abort();
return void cb('NO_SUCH_USER', res); return void cb('NO_SUCH_USER', res);
} }
@ -514,8 +518,23 @@ define([
if (testing) { return void proceed(result); } if (testing) { return void proceed(result); }
if (!(proxy.curvePrivate && proxy.curvePublic &&
proxy.edPrivate && proxy.edPublic)) {
console.log("recovering derived public/private keypairs");
// **** reset keys ****
proxy.curvePrivate = result.curvePrivate;
proxy.curvePublic = result.curvePublic;
proxy.edPrivate = result.edPrivate;
proxy.edPublic = result.edPublic;
}
setTimeout(function () {
Realtime.whenRealtimeSyncs(result.realtime, function () {
proceed(result); proceed(result);
}); });
});
});
}, 500); }, 500);
}, 200); }, 200);
}; };

@ -94,7 +94,7 @@ define([
]) ])
]) ])
]), ]),
h('div.cp-version-footer', "CryptPad v2.6.0 (Gibbon)") h('div.cp-version-footer', "CryptPad v2.8.0 (Ibis)")
]); ]);
}; };
@ -615,6 +615,23 @@ define([
} }
]); ]);
var _link = h('a', {
href: "https://opencollective.com/cryptpad/contribute",
target: '_blank',
rel: 'noopener',
});
var crowdFunding = AppConfig.disableCrowdfundingMessages ? undefined : h('button', [
Msg.crowdfunding_home1,
h('br'),
Msg.crowdfunding_home2,
_link
]);
$(crowdFunding).click(function () {
_link.click();
});
return [ return [
h('div#cp-main', [ h('div#cp-main', [
infopageTopbar(), infopageTopbar(),
@ -629,6 +646,11 @@ define([
icons, icons,
more more
]) ])
]),
h('div.row', [
h('div.cp-crowdfunding', [
crowdFunding
])
]) ])
]), ]),
]), ]),
@ -810,7 +832,7 @@ define([
placeholder: Msg.login_password, placeholder: Msg.login_password,
}), }),
h('div.checkbox-container', [ h('div.checkbox-container', [
Pages.createCheckbox('import-recent', Msg.register_importRecent, true), Pages.createCheckbox('import-recent', Msg.register_importRecent),
]), ]),
h('div.extra', [ h('div.extra', [
h('button.login.first.btn', Msg.login_login) h('button.login.first.btn', Msg.login_login)

@ -43,6 +43,10 @@
//transform: scale(0.1); //transform: scale(0.1);
//transform: scale(1); //transform: scale(1);
h1, h2, h3 {
font-size: 1.5em;
}
.cp-corner-filler { .cp-corner-filler {
float: left; float: left;
clear: left; clear: left;
@ -94,6 +98,8 @@
.cp-corner-footer { .cp-corner-footer {
font-style: italic; font-style: italic;
font-size: 0.8em; font-size: 0.8em;
}
.cp-corner-footer, .cp-corner-text {
a { a {
color: @corner-link; color: @corner-link;
&:hover { &:hover {
@ -103,10 +109,11 @@
} }
button { button {
color: white;
border: 0px; border: 0px;
padding: 5px; padding: 5px;
color: @colortheme_base; color: @colortheme_base;
margin-left: 5px;
outline: none;
&.cp-corner-primary { &.cp-corner-primary {
background-color: @corner-button-ok; background-color: @corner-button-ok;
font-weight: bold; font-weight: bold;

@ -12,6 +12,7 @@
@import (reference) './font.less'; @import (reference) './font.less';
@import (reference) "./app-print.less"; @import (reference) "./app-print.less";
@import (reference) "./app-noscroll.less"; @import (reference) "./app-noscroll.less";
@import (reference) "./messenger.less";
.framework_main(@bg-color, @warn-color, @color) { .framework_main(@bg-color, @warn-color, @color) {
--LessLoader_require: LessLoader_currentFile(); --LessLoader_require: LessLoader_currentFile();
@ -36,6 +37,7 @@
.tippy_main(); .tippy_main();
.checkmark_main(20px); .checkmark_main(20px);
.password_main(); .password_main();
.messenger_main();
.creation_main( .creation_main(
@bg-color: @bg-color, @bg-color: @bg-color,
@color: @color @color: @color

@ -0,0 +1,382 @@
@import (reference) './avatar.less';
@import (reference) "./colortheme-all.less";
.messenger_main() {
--LessLoader_require: LessLoader_currentFile();
};
& {
@keyframes example {
0% {
background: rgba(0,0,0,0.1);
}
50% {
background: rgba(0,0,0,0.3);
}
100% {
background: rgba(0,0,0,0.1);
}
}
@button-border: 2px;
@bg-color: @colortheme_friends-bg;
@color: @colortheme_friends-color;
@room-height: 48px;
#cp-app-contacts-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
min-height: 0;
&.ready {
background-size: cover;
background-position: center;
}
}
.cp-app-contacts-spinner {
display: none;
}
.cp-app-contacts-initializing {
.cp-app-contacts-spinner {
color: white;
display: block;
}
.cp-app-contacts-info {
display: none;
}
#cp-app-contacts-friendlist,
#cp-app-contacts-messaging {
display: flex;
justify-content: center;
align-items: center;
}
}
#cp-app-contacts-friendlist {
width: 200px;
height: 100%;
background-color: lighten(@bg-color, 10%);
overflow-y: auto;
display: flex;
flex-flow: column;
.cp-app-contacts-friend {
background: rgba(0,0,0,0.1);
padding: 5px;
margin: 10px;
margin-bottom: 0;
cursor: pointer;
position: relative;
height: @room-height;
.cp-app-contacts-right-col {
margin-left: 5px;
display: flex;
flex-flow: column;
flex: 1;
min-width: 0;
.cp-app-contacts-name {
white-space: nowrap;
}
}
&:hover {
background-color: rgba(0,0,0,0.3);
}
&.cp-app-contacts-notify {
animation: example 2s ease-in-out infinite;
}
}
.cp-app-contacts-remove {
cursor: pointer;
width: 20px;
&:hover {
color: darken(@color, 20%);
}
}
.cp-app-contacts-category {
display: flex;
flex-flow: column;
flex-grow: 0;
flex-shrink: 0;
.cp-app-contacts-category-title {
order: 1;
font-size: 18px;
margin: 0px 5px;
text-align: center;
background: rgba(0,0,0,0.1);
font-weight: bold;
height: 22px;
line-height: 22px;
}
.cp-app-contacts-category-content {
order: 2;
display: flex;
flex-flow: column-reverse;
padding-bottom: 10px;
&:empty {
display: none;
& ~ .cp-app-contacts-category-title {
display: none;
}
}
}
}
}
#cp-app-contacts-container.cp-app-contacts-inapp {
#cp-app-contacts-friendlist {
display: none;
/*
transition: width 0.2s ease-in-out 0.2s;
width: 68px;
.cp-app-contacts-friend {
.cp-app-contacts-right-col {
overflow: hidden;
}
}
.cp-app-contacts-category-title {
transition: font-size 0.2s ease-in-out 0.2s;
margin: 0px 2px;
font-size: 16px;
}
&:hover {
transition-delay: 1.5s;
width: 200px !important;
.cp-app-contacts-category-title {
transition-delay: 1.5s;
font-size: 18px;
}
}
*/
}
}
#cp-app-contacts-friendlist .cp-app-contacts-friend, #cp-app-contacts-messaging .cp-avatar {
.avatar_main(30px);
&.cp-avatar {
display: flex;
}
cursor: pointer;
color: @color;
media-tag {
img {
color: #000;
}
}
media-tag, .cp-avatar-default {
//margin-right: 5px;
flex-shrink: 0;
z-index: 1;
margin: 4px;
}
.cp-app-contacts-status {
//width: 5px;
display: inline-block;
position: absolute;
//right: 0;
//top: 0;
//bottom: 0;
//opacity: 0.7;
//background-color: #777;
/* width: (@room-height - 6px);
top: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
*/
width: 10px;
height: 10px;
top: 0;
right: 0;
border-bottom-left-radius: 100%;
&.cp-app-contacts-online {
//background-color: green;
//background-color: white;
background-color: #c5ffa8;
}
&.cp-app-contacts-offline {
display: none;
//background-color: red;
}
}
}
.placeholder (@color: #bbb) {
&::-webkit-input-placeholder { /* WebKit, Blink, Edge */
color: @color;
}
&:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
color: @color;
opacity: 1;
}
&::-moz-placeholder { /* Mozilla Firefox 19+ */
color: @color;
opacity: 1;
}
&:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: @color;
}
&::-ms-input-placeholder { /* Microsoft Edge */
color: @color;
}
}
#cp-app-contacts-messaging {
flex: 1;
height: 100%;
background-color: lighten(@bg-color, 20%);
min-width: 0;
.cp-app-contacts-info {
padding: 20px;
}
.cp-app-contacts-header {
background-color: lighten(@bg-color, 15%);
padding: 0;
display: flex;
justify-content: space-between;
align-items: center;
height: 50px;
.hover () {
height: 100%;
line-height: 30px;
padding: 10px;
&:hover {
background-color: rgba(50,50,50,0.3);
}
}
.cp-avatar,
.cp-app-contacts-right-col {
flex: 1 1 auto;
}
.cp-app-contacts-remove-history {
.hover;
}
.cp-avatar {
margin: 10px;
}
.cp-app-contacts-more-history {
//display: none;
.hover;
&.cp-app-contacts-faded {
color: darken(@bg-color, 5%);
}
}
.cp-app-contacts-header-title {
padding: 10px;
flex: 1;
}
}
.cp-app-contacts-tips {
margin: 1em;
background-color: lighten(@bg-color, 15%);
font-size: 14px;
padding: 10px;
position: relative;
.cp-app-contacts-tips-close {
cursor: pointer;
position: absolute;
top: 2px;
right: 2px;
}
}
.cp-app-contacts-chat {
height: 100%;
display: flex;
flex-flow: column;
.cp-app-contacts-messages {
padding: 0 20px;
margin: 10px 0;
flex: 1;
overflow-x: auto;
.cp-app-contacts-message {
display: flex;
flex-wrap: wrap;
& > div {
padding: 0 10px;
}
.cp-app-contacts-content {
overflow: hidden;
word-wrap: break-word;
&> * {
margin: 0;
}
flex: 1;
min-width: 70%;
position: relative;
}
.cp-app-contacts-date {
display: none;
font-style: italic;
}
.cp-app-contacts-sender {
margin-top: 10px;
font-weight: bold;
background-color: rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
width: 100%;
}
.cp-app-contacts-time {
display: none;
font-size: 0.8em;
align-items: center;
color: @color;
font-weight: bold;
position: absolute;
right: 0;
top: 0;
bottom: 0;
background: rgba(0,0,0,0.3);
border-top-left-radius: 50%;
border-bottom-left-radius: 50%;
padding: 0 10px;
}
&:hover {
.cp-app-contacts-time {
display: flex;
}
}
}
}
}
.cp-app-contacts-input {
background-color: lighten(@bg-color, 15%);
height: auto;
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 5%;
textarea {
margin: 5px 0;
padding: 5px 10px;
border: none;
height: 54px; // 2 lines (22px height) + 2 margins (5px)
flex: 1;
background-color: darken(@bg-color, 10%);
color: @color;
resize: none;
overflow-y: auto;
.placeholder(#bbb);
&[disabled="true"] {
.placeholder(#999);
}
}
button {
height: 54px;
border-radius: 0;
border: none;
background-color: darken(@bg-color, 15%);
&:hover {
background-color: darken(@bg-color, 20%);
}
}
}
}
}

@ -72,6 +72,18 @@
.modal_main(); .modal_main();
}; };
& { & {
@keyframes notification {
0% {
background: rgba(0,0,0,0);
}
50% {
background: rgba(0,0,0,0.2);
}
100% {
background: rgba(0,0,0,0);
}
}
.toolbar_vars(); .toolbar_vars();
@toolbar_line-height: 32px; @toolbar_line-height: 32px;
@toolbar_top-height: 64px; @toolbar_top-height: 64px;
@ -134,9 +146,39 @@
} }
} }
.cp-toolbar-userlist-drawer { .cp-toolbar-chat-drawer {
background-color: @toolbar-bg-color; background-color: @toolbar-bg-color;
background-color: var(--toolbar-bg-color); background-color: var(--toolbar-bg-color);
font: @colortheme_app-font-size @colortheme_font;
width: 20%;
min-width: 200px;
display: block;
overflow-y: auto;
overflow-x: hidden;
padding: 0;
box-sizing: border-box;
position: relative;
order: -2;
resize: horizontal;
#cp-app-contacts-container {
height: 100%;
}
.cp-toolbar-chat-drawer-close {
color: @toolbar-color;
color: var(--toolbar-color);
position: absolute;
top: 0;
right: 1px;
font-size: 15px;
opacity: 0.5;
cursor: pointer;
text-shadow: unset;
&:hover {
opacity: 1;
}
}
}
.cp-toolbar-userlist-drawer {
font: @colortheme_app-font-size @colortheme_font; font: @colortheme_app-font-size @colortheme_font;
min-width: 175px; min-width: 175px;
width: 175px; width: 175px;
@ -145,6 +187,7 @@
overflow-x: hidden; overflow-x: hidden;
padding: 10px; padding: 10px;
box-sizing: border-box; box-sizing: border-box;
order: -3;
.cp-toolbar-userlist-drawer-close { .cp-toolbar-userlist-drawer-close {
position: absolute; position: absolute;
margin-top: -10px; margin-top: -10px;
@ -219,6 +262,14 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
button {
width: 20px;
font-size: 16px;
padding: 0;
border: none;
height: 20px;
cursor: pointer;
}
} }
.cp-toolbar-userlist-name-input { .cp-toolbar-userlist-name-input {
flex: 1; flex: 1;
@ -235,14 +286,6 @@
min-height: 0; min-height: 0;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.cp-toolbar-userlist-name-edit {
width: 20px;
font-size: 16px;
padding: 0;
border: none;
height: 20px;
cursor: pointer;
}
.cp-toolbar-userlist-friend { .cp-toolbar-userlist-friend {
padding: 0; padding: 0;
} }
@ -276,11 +319,15 @@
margin: 50px; margin: 50px;
} }
&> div { &> div {
flex: 1;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
align-content: center; align-content: center;
overflow-y: auto; }
&> div:last-child {
flex: unset;
margin: 50px 0;
} }
} }
@ -299,13 +346,15 @@
margin-left: 5px; margin-left: 5px;
} }
@media screen and (max-height: @browser_media-not-big) { @media screen and (max-height: @browser_media-medium-screen),
screen and (max-width: @browser_media-medium-screen) {
.cp-modal { .cp-modal {
& > p { & > p {
display: none; display: none;
} }
& > div { & > div {
align-content: unset; align-content: unset;
align-items: center;
li { li {
height: 40px; height: 40px;
width: 200px; width: 200px;
@ -313,9 +362,11 @@
align-items: center; align-items: center;
.fa { .fa {
font-size: 32px; font-size: 32px;
min-width: 50px;
} }
.cp-icons-name { .cp-icons-name {
height: auto; height: auto;
text-align: left;
} }
} }
} }
@ -344,7 +395,7 @@
color: @toolbar-color; color: @toolbar-color;
color: var(--toolbar-color); color: var(--toolbar-color);
} }
.cp-toolbar-userlist-name-edit { .cp-toolbar-userlist-button {
color: @toolbar-userlist-name-edit; color: @toolbar-userlist-name-edit;
color: var(--toolbar-userlist-name-edit); color: var(--toolbar-userlist-name-edit);
background: transparent; background: transparent;
@ -961,7 +1012,8 @@
height: @toolbar_line-height; height: @toolbar_line-height;
} }
#cp-toolbar-userlist-drawer-open { order: 1; } #cp-toolbar-userlist-drawer-open { order: 0; }
#cp-toolbar-chat-drawer-open { order: 1; }
.cp-toolbar-share-button { order: 2; } .cp-toolbar-share-button { order: 2; }
.cp-toolbar-spinner { order: 3; } .cp-toolbar-spinner { order: 3; }
@ -969,6 +1021,11 @@
width: 125px; width: 125px;
text-align: center; text-align: center;
} }
#cp-toolbar-chat-drawer-open button {
&.cp-toolbar-notification {
animation: notification 2s ease-in-out infinite;
}
}
.cp-toolbar-share-button { .cp-toolbar-share-button {
width: 50px; width: 50px;
text-align: center; text-align: center;

@ -180,6 +180,24 @@
} }
} }
} }
.cp-crowdfunding {
width: 100%;
text-align: center;
button {
outline: none;
background-color: @colortheme_logo-2;
color: @colortheme_base;
border: none;
padding: 10px 20px;
border-radius: 44px;
cursor: pointer;
&:hover {
background-color: lighten(@colortheme_logo-2, 3%);
}
}
}
@media (min-width: 576px) and (max-width: 767px) { @media (min-width: 576px) and (max-width: 767px) {
.container { .container {
padding-left: 0; padding-left: 0;

@ -567,6 +567,14 @@ define(function () {
out.settings_importConfirm = "Bist Du sicher, dass Du die kürzlich besuchte Dokumente in Deinem Konto importieren möchtest??"; out.settings_importConfirm = "Bist Du sicher, dass Du die kürzlich besuchte Dokumente in Deinem Konto importieren möchtest??";
out.settings_importDone = "Import erledigt"; out.settings_importDone = "Import erledigt";
out.settings_autostoreTitle = "Automatisches Speichern im CryptDrive";
out.settings_autostoreHint = "<b>Automatisch:</b> Alle Pads werden in deinem CryptDrive gespeichert.<br>" +
"<b>Manuell (immer nachfragen):</b> Wenn du ein Pad noch nicht gespeichert hast, wirst du gefragt, ob du es im CryptDrive speichern willst.<br>" +
"<b>Manuell (nie nachfragen):</b> Pads werden nicht automatisch im CryptDrive gespeichert. Die Option, sie trotzdem zu speichern, ist versteckt.<br>";
out.settings_autostoreYes = "Automatisch";
out.settings_autostoreNo = "Manuell (nie nachfragen)";
out.settings_autostoreMaybe = "Manual (immer nachfragen)";
out.settings_userFeedbackTitle = "Rückmeldung"; out.settings_userFeedbackTitle = "Rückmeldung";
out.settings_userFeedbackHint1 = "CryptPad gibt grundlegende Rückmeldungen zum Server, um die Benutzer-Erfahrung zu verbessern können."; out.settings_userFeedbackHint1 = "CryptPad gibt grundlegende Rückmeldungen zum Server, um die Benutzer-Erfahrung zu verbessern können.";
out.settings_userFeedbackHint2 = "Der Inhalt deiner Dokumente wird nie mit dem Server geteilt."; out.settings_userFeedbackHint2 = "Der Inhalt deiner Dokumente wird nie mit dem Server geteilt.";
@ -1107,7 +1115,7 @@ define(function () {
out.readme_cat2_l2 = "Der Titel eines Dokuments kann mit einem Klick auf den Stift geändert werden."; out.readme_cat2_l2 = "Der Titel eines Dokuments kann mit einem Klick auf den Stift geändert werden.";
out.readme_cat3 = "Entdecke CryptPad Apps"; out.readme_cat3 = "Entdecke CryptPad Apps";
out.readme_cat3_l1 = "Mit dem CryptPad Codeeditor kannst du Code wie JavaScript, Markdown, oder HTML bearbeiten"; out.readme_cat3_l1 = "Mit dem CryptPad Codeeditor kannst du Code wie JavaScript, Markdown, oder HTML bearbeiten";
out.readme_cat3_l2 = "Mit dem CryptPad Präsentationseditor kannst du schnell Vorträge mit Hilfe von Markdwon gestalten"; out.readme_cat3_l2 = "Mit dem CryptPad Präsentationseditor kannst du schnell Vorträge mit Hilfe von Markdown gestalten";
out.readme_cat3_l3 = "Mit der CryptPad Umfrage kannst du schnell Abstimmungen durchführen, insbesondere, um Meetings zu planen, die in den Kalender von allen passen."; out.readme_cat3_l3 = "Mit der CryptPad Umfrage kannst du schnell Abstimmungen durchführen, insbesondere, um Meetings zu planen, die in den Kalender von allen passen.";
// Tips // Tips
@ -1200,6 +1208,27 @@ define(function () {
out.loading_drive_2 = "Aktualisiere Datenformat"; out.loading_drive_2 = "Aktualisiere Datenformat";
out.loading_drive_3 = "Verifiziere Datenintegrität"; out.loading_drive_3 = "Verifiziere Datenintegrität";
// Shared folders
out.sharedFolders_forget = "Dieses pad wird nur in einem geteilten Ordner gespeichert, du kannst es nicht in den Papierkorb verschieben. Du kannst es in deinem CryptDrive löschen.";
out.sharedFolders_duplicate = "Einige der pads, die du versucht hast zu verschieben, waren schon im Zielordner geteilt.";
out.sharedFolders_create = "Erstelle einen geteilten Ordner";
out.sharedFolders_create_name = "Neuer Ordner";
out.sharedFolders_create_owned = "Eigener Ordner";
out.sharedFolders_create_password = "Ordnerpasswort";
out.sharedFolders_share = "Teile diese URL mit anderen registrierten Benutzern, um ihnen Zugriff auf den geteilten Ordner zu geben. Sobald sie diese URL öffnen, wird der geteilte Ordner zu ihrem CryptDrive hinzugefügt.";
out.chrome68 = "Anscheinend benutzt du Chrome oder Chromium version 68. Darin ist ein bug, der dafür sorgt, dass nach ein paar Sekunden die Seite komplett weiß ist oder nicht mehr auf Klicks reagiert. Um das Problem zu beheben, wechsle den Tab und komme wieder, oder versuche zu scrollen. Dieser Bug sollte in der nächsten Version deines Browsers gefixt sein.";
// Manual pad storage popup
out.autostore_notstored = "Dieses Pad ist noch nicht in deinem CryptDrive. Willst du es jetzt speichern?";
out.autostore_settings = "Du kannst automatisches Speichern im CryptDrive in deinen <a href=\"/settings/\">Einstellungen</a> aktivieren!";
out.autostore_store = "Speichern";
out.autostore_hide = "Nicht speichern";
out.autostore_error = "Unerwarteter Fehler: wir konnten das Pad nicht speichern, bitte versuche es nochmal.";
out.autostore_saved = "Das Pad wurde erfolgreich in deinem CryptDrive gespeichert!";
out.autostore_forceSave = "Speicher die Datei in deinem CryptDrive"; // File upload modal
out.autostore_notAvailable = "Du musst dieses Pad in deinem CryptDrive speichern, bevor du dieses Feature benutzen kannst."; // Properties/tags/move to trash
return out; return out;
}); });

@ -141,6 +141,8 @@ define(function () {
out.userListButton = "Liste d'utilisateurs"; out.userListButton = "Liste d'utilisateurs";
out.chatButton = "Chat";
out.userAccountButton = "Votre compte"; out.userAccountButton = "Votre compte";
out.newButton = 'Nouveau'; out.newButton = 'Nouveau';
@ -226,6 +228,7 @@ define(function () {
out.notifyRenamed = "{0} a changé son nom en {1}"; out.notifyRenamed = "{0} a changé son nom en {1}";
out.notifyLeft = "{0} a quitté la session collaborative"; out.notifyLeft = "{0} a quitté la session collaborative";
out.ok = 'OK';
out.okButton = 'OK (Entrée)'; out.okButton = 'OK (Entrée)';
out.cancel = "Annuler"; out.cancel = "Annuler";
@ -252,6 +255,11 @@ define(function () {
out.pad_mediatagTitle = "Options du Media-Tag"; out.pad_mediatagTitle = "Options du Media-Tag";
out.pad_mediatagWidth = "Largeur (px)"; out.pad_mediatagWidth = "Largeur (px)";
out.pad_mediatagHeight = "Hauteur (px)"; out.pad_mediatagHeight = "Hauteur (px)";
out.pad_mediatagRatio = "Préserver les proportions";
out.pad_mediatagBorder = "Éaisseur de la bordure (px)";
out.pad_mediatagPreview = "Aperçu";
out.pad_mediatagImport = 'Sauver dans votre CryptDrive';
out.pad_mediatagOptions = 'Propriétés de l\'image';
// Kanban // Kanban
out.kanban_newBoard = "Nouveau tableau"; out.kanban_newBoard = "Nouveau tableau";
@ -370,7 +378,8 @@ define(function () {
out.contacts_remove = 'Supprimer ce contact'; out.contacts_remove = 'Supprimer ce contact';
out.contacts_confirmRemove = 'Êtes-vous sûr de vouloir supprimer <em>{0}</em> de vos contacts ?'; out.contacts_confirmRemove = 'Êtes-vous sûr de vouloir supprimer <em>{0}</em> de vos contacts ?';
out.contacts_typeHere = "Entrez un message ici..."; out.contacts_typeHere = "Entrez un message ici...";
out.contacts_warning = "Tout ce que vous tapez ici est permanent et visible par tous les utilisateurs actuels et futurs de ce pad. Soyez prudent avec vos données confidentielles !";
out.contacts_padTitle = "Chat";
out.contacts_info1 = "Voici vos contacts. Ici, vous pouvez :"; out.contacts_info1 = "Voici vos contacts. Ici, vous pouvez :";
out.contacts_info2 = "Cliquer sur le nom d'un contact pour discuter avec lui"; out.contacts_info2 = "Cliquer sur le nom d'un contact pour discuter avec lui";
@ -382,6 +391,12 @@ define(function () {
out.contacts_removeHistoryServerError = 'Une erreur est survenue lors de la supprimer de l\'historique du chat. Veuillez réessayer plus tard.'; out.contacts_removeHistoryServerError = 'Une erreur est survenue lors de la supprimer de l\'historique du chat. Veuillez réessayer plus tard.';
out.contacts_fetchHistory = "Récupérer l'historique plus ancien"; out.contacts_fetchHistory = "Récupérer l'historique plus ancien";
out.contacts_friends = "Amis";
out.contacts_rooms = "Salons";
out.contacts_leaveRoom = "Quitter ce salon";
out.contacts_online = "Un autre utilisateur est en ligne dans ce salon";
// File manager // File manager
out.fm_rootName = "Documents"; out.fm_rootName = "Documents";
@ -414,6 +429,7 @@ define(function () {
out.fm_noname = "Document sans titre"; out.fm_noname = "Document sans titre";
out.fm_emptyTrashDialog = "Êtes-vous sûr de vouloir vider la corbeille ?"; out.fm_emptyTrashDialog = "Êtes-vous sûr de vouloir vider la corbeille ?";
out.fm_removeSeveralPermanentlyDialog = "Êtes-vous sûr de vouloir supprimer ces {0} éléments de votre CryptDrive de manière permanente ?"; out.fm_removeSeveralPermanentlyDialog = "Êtes-vous sûr de vouloir supprimer ces {0} éléments de votre CryptDrive de manière permanente ?";
out.fm_removePermanentlyNote = "Les pads dont vous êtes le propriétaire seront supprimés du serveur.";
out.fm_removePermanentlyDialog = "Êtes-vous sûr de vouloir supprimer cet élément de votre CryptDrive de manière permanente ?"; out.fm_removePermanentlyDialog = "Êtes-vous sûr de vouloir supprimer cet élément de votre CryptDrive de manière permanente ?";
out.fm_deleteOwnedPad = "Êtes-vous sûr de vouloir supprimer définitivement ce pad du serveur ?"; out.fm_deleteOwnedPad = "Êtes-vous sûr de vouloir supprimer définitivement ce pad du serveur ?";
out.fm_deleteOwnedPads = "Êtes-vous sûr de vouloir supprimer définitivement ces pads du serveur ?"; out.fm_deleteOwnedPads = "Êtes-vous sûr de vouloir supprimer définitivement ces pads du serveur ?";
@ -574,6 +590,14 @@ define(function () {
out.settings_importConfirm = "Êtes-vous sûr de vouloir importer les pads récents de ce navigateur dans le CryptDrive de votre compte utilisateur ?"; out.settings_importConfirm = "Êtes-vous sûr de vouloir importer les pads récents de ce navigateur dans le CryptDrive de votre compte utilisateur ?";
out.settings_importDone = "Importation terminée"; out.settings_importDone = "Importation terminée";
out.settings_autostoreTitle = "Stockage des pads dans CryptDrive";
out.settings_autostoreHint = "Le stockage <b>Automatique</b> des pads permet de sauver tous les pads que vous visitez dans votre CryptDrive, sans action de votre part.<br>" +
"Le stockage <b>Manuel (toujours demander)</b> permet de ne pas stocker automatiquement les pads, mais d'afficher un message vous demandant s'il faut le faire ou non.<br>" +
"Le stockage <b>Manuel (ne pas demander)</b> permet de ne pas stocker les pads ni d'afficher le message. Une option permettant de les stocker sera toujours disponible, mais cachée.";
out.settings_autostoreYes = "Automatique";
out.settings_autostoreNo = "Manuel (ne pas demander)";
out.settings_autostoreMaybe = "Manuel (toujours demander)";
out.settings_userFeedbackTitle = "Retour d'expérience"; out.settings_userFeedbackTitle = "Retour d'expérience";
out.settings_userFeedbackHint1 = "CryptPad peut envoyer des retours d'expérience très limités vers le serveur, de manière à nous permettre d'améliorer l'expérience des utilisateurs. "; out.settings_userFeedbackHint1 = "CryptPad peut envoyer des retours d'expérience très limités vers le serveur, de manière à nous permettre d'améliorer l'expérience des utilisateurs. ";
out.settings_userFeedbackHint2 = "Le contenu de vos pads et les clés de déchiffrement ne seront jamais partagés avec le serveur."; out.settings_userFeedbackHint2 = "Le contenu de vos pads et les clés de déchiffrement ne seront jamais partagés avec le serveur.";
@ -659,6 +683,7 @@ define(function () {
// pad // pad
out.pad_showToolbar = "Afficher la barre d'outils"; out.pad_showToolbar = "Afficher la barre d'outils";
out.pad_hideToolbar = "Cacher la barre d'outils"; out.pad_hideToolbar = "Cacher la barre d'outils";
out.pad_base64 = "Ce pad contient des images stockées de manière inefficace. Ces images vont augmenter de manière significative la taille du pad dans votre CryptDrive, et le rendre plus lent à charger. Vous pouvez migrer ces fichiers afin de les stocker séparément dans votre CryptDrive. Voulez-vous commencer la migration maintenant?";
// markdown toolbar // markdown toolbar
out.mdToolbar_button = "Afficher ou cacher la barre d'outils Markdown"; out.mdToolbar_button = "Afficher ou cacher la barre d'outils Markdown";
@ -1190,5 +1215,28 @@ define(function () {
out.sharedFolders_create_password = "Mot de passe du dossier"; out.sharedFolders_create_password = "Mot de passe du dossier";
out.sharedFolders_share = "Partager cette URL avec d'autres utilisateurs enregistrés leur donne accès au dossier partagé. Une fois l'URL ouverte, le dossier partagé sera ajouté au répertoire racine de leur CryptDrive."; out.sharedFolders_share = "Partager cette URL avec d'autres utilisateurs enregistrés leur donne accès au dossier partagé. Une fois l'URL ouverte, le dossier partagé sera ajouté au répertoire racine de leur CryptDrive.";
out.chrome68 = "Il semblerait que vous utilisiez le navigateur Chrome version 68. Ce navigateur contient un bug rendant certaines pages entièrement blanches après quelques secondes ou bloquant les clics. Pour corriger ce problème, vous pouvez vous déplacer vers un nouvel onglet et revenir ou vous pouvez essayer de faire défiler la page. Ce bug devrait être corrigé dans la prochaine version du navigateur.";
// Manual pad storage popup
out.autostore_notstored = "Ce pad n'est pas dans votre CryptDrive. Souhaitez-vous le stocker ?";
out.autostore_settings = "Vous pouvez activer le stockage automatique des pads dans vos <a href=\"/settings/\">Préférences</a> !";
out.autostore_store = "Stocker";
out.autostore_hide = "Ne pas stocker";
out.autostore_error = "Erreur : nous n'avons pas réussi à stocker ce pad, veuillez ré-essayer.";
out.autostore_saved = "Ce pad a été stocké avec succès dans votre CryptDrive !";
out.autostore_forceSave = "Stocker le fichier dans votre CryptDrive"; // File upload modal
out.autostore_notAvailable = "Vous devez stocker ce pad dans votre CryptDrive avant de pouvoir utiliser cette fonctionnalité.";
// Crowdfunding messages
out.crowdfunding_home1 = "CryptPad a besoin d'aide !";
out.crowdfunding_home2 = "Cliquez pour découvrir notre campagne de financement participatif.";
out.crowdfunding_popup_text = "<h3>Aider CryptPad</h3>" +
"Pour vous assurer que CryptPad soit activement développé, nous vous invitons à supporter le projet via la " +
'<a href="https://opencollective.com/cryptpad">page OpenCollective</a>, où vous pouvez trouver notre <b>Roadmap</b> et nos <b>objectifs de financement</b>.';
out.crowdfunding_popup_yes = "Voir la page";
out.crowdfunding_popup_no = "Pas maintenant";
out.crowdfunding_popup_never = "Ne plus demander";
return out; return out;
}); });

@ -142,6 +142,8 @@ define(function () {
out.userListButton = "User list"; out.userListButton = "User list";
out.chatButton = "Chat";
out.userAccountButton = "Your account"; out.userAccountButton = "Your account";
out.newButton = 'New'; out.newButton = 'New';
@ -258,7 +260,7 @@ define(function () {
out.pad_mediatagRatio = "Keep ratio"; out.pad_mediatagRatio = "Keep ratio";
out.pad_mediatagBorder = "Border width (px)"; out.pad_mediatagBorder = "Border width (px)";
out.pad_mediatagPreview = "Preview"; out.pad_mediatagPreview = "Preview";
out.pad_mediatagImport = 'Save in CryptDrive'; out.pad_mediatagImport = 'Save in your CryptDrive';
out.pad_mediatagOptions = 'Image properties'; out.pad_mediatagOptions = 'Image properties';
// Kanban // Kanban
@ -378,6 +380,8 @@ define(function () {
out.contacts_remove = 'Remove this contact'; out.contacts_remove = 'Remove this contact';
out.contacts_confirmRemove = 'Are you sure you want to remove <em>{0}</em> from your contacts?'; out.contacts_confirmRemove = 'Are you sure you want to remove <em>{0}</em> from your contacts?';
out.contacts_typeHere = "Type a message here..."; out.contacts_typeHere = "Type a message here...";
out.contacts_warning = "Everything you type here is persistent and available to all the existing and future users of this pad. Be careful with sensitive information!";
out.contacts_padTitle = "Chat";
out.contacts_info1 = "These are your contacts. From here, you can:"; out.contacts_info1 = "These are your contacts. From here, you can:";
out.contacts_info2 = "Click your contact's icon to chat with them"; out.contacts_info2 = "Click your contact's icon to chat with them";
@ -389,6 +393,12 @@ define(function () {
out.contacts_removeHistoryServerError = 'There was an error while removing your chat history. Try again later'; out.contacts_removeHistoryServerError = 'There was an error while removing your chat history. Try again later';
out.contacts_fetchHistory = "Retrieve older history"; out.contacts_fetchHistory = "Retrieve older history";
out.contacts_friends = "Friends";
out.contacts_rooms = "Rooms";
out.contacts_leaveRoom = "Leave this room";
out.contacts_online = "Another user from this room is online";
// File manager // File manager
out.fm_rootName = "Documents"; out.fm_rootName = "Documents";
@ -420,12 +430,13 @@ define(function () {
out.fm_openParent = "Show in folder"; out.fm_openParent = "Show in folder";
out.fm_noname = "Untitled Document"; out.fm_noname = "Untitled Document";
out.fm_emptyTrashDialog = "Are you sure you want to empty the trash?"; out.fm_emptyTrashDialog = "Are you sure you want to empty the trash?";
out.fm_removeSeveralPermanentlyDialog = "Are you sure you want to remove these {0} elements from your CryptDrive permanently?"; out.fm_removeSeveralPermanentlyDialog = "Are you sure you want to permanently remove these {0} elements from your CryptDrive?";
out.fm_removePermanentlyDialog = "Are you sure you want to remove that element from your CryptDrive permanently?"; out.fm_removePermanentlyNote = "Owned pads will be removed from the server if you continue.";
out.fm_removePermanentlyDialog = "Are you sure you want to permanently remove that element from your CryptDrive?";
out.fm_removeSeveralDialog = "Are you sure you want to move these {0} elements to the trash?"; out.fm_removeSeveralDialog = "Are you sure you want to move these {0} elements to the trash?";
out.fm_removeDialog = "Are you sure you want to move {0} to the trash?"; out.fm_removeDialog = "Are you sure you want to move {0} to the trash?";
out.fm_deleteOwnedPad = "Are you sure you want to remove permanently this pad from the server?"; out.fm_deleteOwnedPad = "Are you sure you want to permanently remove this pad from the server?";
out.fm_deleteOwnedPads = "Are you sure you want to remove permanently these pads from the server?"; out.fm_deleteOwnedPads = "Are you sure you want to permanently remove these pads from the server?";
out.fm_restoreDialog = "Are you sure you want to restore {0} to its previous location?"; out.fm_restoreDialog = "Are you sure you want to restore {0} to its previous location?";
out.fm_unknownFolderError = "The selected or last visited directory no longer exist. Opening the parent folder..."; out.fm_unknownFolderError = "The selected or last visited directory no longer exist. Opening the parent folder...";
out.fm_contextMenuError = "Unable to open the context menu for that element. If the problem persist, try to reload the page."; out.fm_contextMenuError = "Unable to open the context menu for that element. If the problem persist, try to reload the page.";
@ -585,9 +596,9 @@ define(function () {
out.settings_importDone = "Import completed"; out.settings_importDone = "Import completed";
out.settings_autostoreTitle = "Pad storage in CryptDrive"; out.settings_autostoreTitle = "Pad storage in CryptDrive";
out.settings_autostoreHint = "<b>Automatic</b> pad storage results in all the pads you visit being stored in your CryptDrive.<br>" + out.settings_autostoreHint = "<b>Automatic</b> All the pads you visit are stored in your CryptDrive.<br>" +
"<b>Manual (always ask)</b> results in the pads not being stored but a reminder will appear to ask you if you want to store them in CryptDrive.<br>" + "<b>Manual (always ask)</b> If you have not stored a pad yet, you will be asked if you want to store them in your CryptDrive.<br>" +
"<b>Manual (never ask)</b> results in the pads not being stored and option to store them will be available but in a hidden way."; "<b>Manual (never ask)</b> Pads are not stored automatically in your Cryptpad. The option to store them will be hidden.";
out.settings_autostoreYes = "Automatic"; out.settings_autostoreYes = "Automatic";
out.settings_autostoreNo = "Manual (never ask)"; out.settings_autostoreNo = "Manual (never ask)";
out.settings_autostoreMaybe = "Manual (always ask)"; out.settings_autostoreMaybe = "Manual (always ask)";
@ -682,7 +693,7 @@ define(function () {
// pad // pad
out.pad_showToolbar = "Show toolbar"; out.pad_showToolbar = "Show toolbar";
out.pad_hideToolbar = "Hide toolbar"; out.pad_hideToolbar = "Hide toolbar";
out.pad_base64 = "This pad contains images stored in an inefficient way. These images will increase significantly the size of the pad in your CryptDrive, and they will make it slower to load. Do you want to migrate these images to a better format (they will be stored separately in your drive)?"; // XXX out.pad_base64 = "This pad contains images stored in an inefficient way. These images will significantly increase the size of the pad in your CryptDrive, and make it slower to load. You can migrate these files to a new format which will be stored separately in your CryptDrive. Do you want to migrate these images now?";
// markdown toolbar // markdown toolbar
out.mdToolbar_button = "Show or hide the Markdown toolbar"; out.mdToolbar_button = "Show or hide the Markdown toolbar";
@ -1257,14 +1268,25 @@ define(function () {
out.chrome68 = "It seems that you're using the browser Chrome or Chromium version 68. It contains a bug resulting in the page turning completely white after a few seconds or the page being unresponsive to clicks. To fix this issue, you can switch to another tab and come back, or try to scroll in the page. This bug should be fixed in the next version of your browser."; out.chrome68 = "It seems that you're using the browser Chrome or Chromium version 68. It contains a bug resulting in the page turning completely white after a few seconds or the page being unresponsive to clicks. To fix this issue, you can switch to another tab and come back, or try to scroll in the page. This bug should be fixed in the next version of your browser.";
// Manual pad storage popup // Manual pad storage popup
out.autostore_notstored = "This pad is not in your CryptDrive. Do you want to store it now?"; // XXX out.autostore_notstored = "This pad is not in your CryptDrive. Do you want to store it now?";
out.autostore_settings = "You can enable automatic pad storage in your <a href=\"/settings/\">Settings</a> page!"; // XXX out.autostore_settings = "You can enable automatic pad storage in your <a href=\"/settings/\">Settings</a> page!";
out.autostore_store = "Store"; out.autostore_store = "Store";
out.autostore_hide = "Don't store"; out.autostore_hide = "Don't store";
out.autostore_error = "Unexpected error: we were unable to store this pad, please try again."; out.autostore_error = "Unexpected error: we were unable to store this pad, please try again.";
out.autostore_saved = "The pad was successfully stored in your CryptDrive!"; out.autostore_saved = "The pad was successfully stored in your CryptDrive!";
out.autostore_forceSave = "Store the file in CryptDrive"; // File upload modal out.autostore_forceSave = "Store the file in your CryptDrive"; // File upload modal
out.autostore_notAvailable = "You must store this pad in your CryptDrive before being able to use this feature."; // Properties/tags/move to trash out.autostore_notAvailable = "You must store this pad in your CryptDrive before being able to use this feature."; // Properties/tags/move to trash
// Crowdfunding messages
out.crowdfunding_home1 = "CryptPad needs your help!";
out.crowdfunding_home2 = "Click to learn about our crowdfunding campaign.";
out.crowdfunding_popup_text = "<h3>We need your help!</h3>" +
"To ensure that CryptPad is actively developed, consider supporting the project via the " +
'<a href="https://opencollective.com/cryptpad">OpenCollective page</a>, where you can see our <b>Roadmap</b> and <b>Funding goals</b>.';
out.crowdfunding_popup_yes = "Go to OpenCollective";
out.crowdfunding_popup_no = "Not now";
out.crowdfunding_popup_never = "Don't ask me again";
return out; return out;
}); });

@ -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": "2.6.0", "version": "2.8.0",
"license": "AGPL-3.0+", "license": "AGPL-3.0+",
"repository": { "repository": {
"type": "git", "type": "git",

@ -19,17 +19,16 @@
flex-flow: column; flex-flow: column;
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;
width: 50%;
min-width: 20%; min-width: 20%;
max-width: 80%; max-width: 80%;
resize: horizontal; resize: horizontal;
overflow: hidden; overflow: hidden;
width: 50%;
&.cp-app-code-fullpage { &.cp-app-code-fullpage {
max-width: 100%; max-width: 100%;
resize: none; resize: none;
flex: 1; flex: 1;
} }
} }
.CodeMirror { .CodeMirror {
flex: 1; flex: 1;
@ -51,9 +50,13 @@
#cp-app-code-container { display: none; } #cp-app-code-container { display: none; }
#cp-app-code-preview { border: 0; } #cp-app-code-preview { border: 0; }
} }
&.cp-chat-visible {
#cp-app-code-container {
width: 35%;
}
}
} }
#cp-app-code-preview { #cp-app-code-preview {
flex: 1;
padding: 5px 20px; padding: 5px 20px;
overflow: auto; overflow: auto;
display: inline-block; display: inline-block;
@ -63,6 +66,7 @@
font-family: Calibri,Ubuntu,sans-serif; font-family: Calibri,Ubuntu,sans-serif;
word-wrap: break-word; word-wrap: break-word;
position: relative; position: relative;
flex: 1;
media-tag { media-tag {
* { * {
max-width:100%; max-width:100%;

@ -134,7 +134,7 @@ define(function() {
// spontaneously, resulting in the deletion of the entire folder's content. // spontaneously, resulting in the deletion of the entire folder's content.
// We highly recommend to keep them disabled until they are stable enough to be enabled // We highly recommend to keep them disabled until they are stable enough to be enabled
// by default by the CryptPad developers. // by default by the CryptPad developers.
config.disableSharedFolders = true; config.disableSharedFolders = false;
return config; return config;
}); });

@ -13,6 +13,8 @@ define(function () {
storageKey: 'filesData', storageKey: 'filesData',
tokenKey: 'loginToken', tokenKey: 'loginToken',
displayPadCreationScreen: 'displayPadCreationScreen', displayPadCreationScreen: 'displayPadCreationScreen',
deprecatedKey: 'deprecated' deprecatedKey: 'deprecated',
// Sub
plan: 'CryptPad_plan'
}; };
}); });

@ -892,7 +892,7 @@ define([
h('div.cp-corner-filler', { style: "width:60px;" }), h('div.cp-corner-filler', { style: "width:60px;" }),
h('div.cp-corner-filler', { style: "width:40px;" }), h('div.cp-corner-filler', { style: "width:40px;" }),
h('div.cp-corner-filler', { style: "width:20px;" }), h('div.cp-corner-filler', { style: "width:20px;" }),
h('div.cp-corner-text', text), Pages.setHTML(h('div.cp-corner-text'), text),
h('div.cp-corner-actions', actions), h('div.cp-corner-actions', actions),
Pages.setHTML(h('div.cp-corner-footer'), footer) Pages.setHTML(h('div.cp-corner-footer'), footer)
]); ]);

@ -150,7 +150,8 @@ define([
} }
cfg.friendComplete({ cfg.friendComplete({
logText: Messages.contacts_added, logText: Messages.contacts_added,
netfluxId: sender netfluxId: sender,
friend: msgData
}); });
var msg = ["FRIEND_REQ_ACK", chan]; var msg = ["FRIEND_REQ_ACK", chan];
var msgStr = Crypto.encrypt(JSON.stringify(msg), key); var msgStr = Crypto.encrypt(JSON.stringify(msg), key);
@ -163,7 +164,7 @@ define([
if (i !== -1) { pendingRequests.splice(i, 1); } if (i !== -1) { pendingRequests.splice(i, 1); }
cfg.friendComplete({ cfg.friendComplete({
logText: Messages.contacts_rejected, logText: Messages.contacts_rejected,
netfluxId: sender netfluxId: sender,
}); });
cfg.updateMetadata(); cfg.updateMetadata();
return; return;
@ -180,7 +181,8 @@ define([
} }
cfg.friendComplete({ cfg.friendComplete({
logText: Messages.contacts_added, logText: Messages.contacts_added,
netfluxId: sender netfluxId: sender,
friend: data
}); });
}); });
return; return;

@ -5,7 +5,9 @@ define([
'/common/common-util.js', '/common/common-util.js',
'/common/common-realtime.js', '/common/common-realtime.js',
'/common/common-constants.js', '/common/common-constants.js',
], function (Crypto, Curve, Hash, Util, Realtime, Constants) {
'/bower_components/nthen/index.js',
], function (Crypto, Curve, Hash, Util, Realtime, Constants, nThen) {
'use strict'; 'use strict';
var Msg = { var Msg = {
inputs: [], inputs: [],
@ -52,7 +54,7 @@ define([
var msgAlreadyKnown = function (channel, sig) { var msgAlreadyKnown = function (channel, sig) {
return channel.messages.some(function (message) { return channel.messages.some(function (message) {
return message[0] === sig; return message.sig === sig;
}); });
}; };
@ -65,6 +67,7 @@ define([
update: [], update: [],
friend: [], friend: [],
unfriend: [], unfriend: [],
event: []
}, },
range_requests: {}, range_requests: {},
}; };
@ -73,6 +76,12 @@ define([
messenger.handlers[type].forEach(g); messenger.handlers[type].forEach(g);
}; };
var emit = function (ev, data) {
eachHandler('event', function (f) {
f(ev, data);
});
};
messenger.on = function (type, f) { messenger.on = function (type, f) {
var stack = messenger.handlers[type]; var stack = messenger.handlers[type];
if (!Array.isArray(stack)) { if (!Array.isArray(stack)) {
@ -95,20 +104,26 @@ define([
Msg.hk = network.historyKeeper; Msg.hk = network.historyKeeper;
var friends = getFriendList(proxy); var friends = getFriendList(proxy);
var getChannel = function (curvePublic) { var getChannel = function (chanId) {
var friend = friends[curvePublic];
if (!friend) { return; }
var chanId = friend.channel;
if (!chanId) { return; }
return channels[chanId]; return channels[chanId];
}; };
var initRangeRequest = function (txid, curvePublic, sig, cb) { var getFriendFromChannel = function (id) {
var friend;
for (var k in friends) {
if (friends[k].channel === id) {
friend = friends[k];
break;
}
}
return friend;
};
var initRangeRequest = function (txid, chanId, cb) {
messenger.range_requests[txid] = { messenger.range_requests[txid] = {
messages: [], messages: [],
cb: cb, cb: cb,
curvePublic: curvePublic, chanId: chanId,
sig: sig,
}; };
}; };
@ -120,24 +135,22 @@ define([
delete messenger.range_requests[txid]; delete messenger.range_requests[txid];
}; };
messenger.getMoreHistory = function (curvePublic, hash, count, cb) { messenger.getMoreHistory = function (chanId, hash, count, cb) {
if (typeof(cb) !== 'function') { return; } if (typeof(cb) !== 'function') { return; }
if (typeof(hash) !== 'string') { if (typeof(hash) !== 'string') {
// FIXME hash is not necessarily defined. // Channel is empty!
// What does this mean? return void cb(void 0, []);
console.error("not sure what to do here");
return;
} }
var chan = getChannel(curvePublic); var chan = getChannel(chanId);
if (typeof(chan) === 'undefined') { if (typeof(chan) === 'undefined') {
console.error("chan is undefined. we're going to have a problem here"); console.error("chan is undefined. we're going to have a problem here");
return; return;
} }
var txid = Util.uid(); var txid = Util.uid();
initRangeRequest(txid, curvePublic, hash, cb); initRangeRequest(txid, chanId, cb);
var msg = [ 'GET_HISTORY_RANGE', chan.id, { var msg = [ 'GET_HISTORY_RANGE', chan.id, {
from: hash, from: hash,
count: count, count: count,
@ -151,38 +164,80 @@ define([
}); });
}; };
var getCurveForChannel = function (id) { /*var getCurveForChannel = function (id) {
var channel = channels[id]; var channel = channels[id];
if (!channel) { return; } if (!channel) { return; }
return channel.curve; return channel.curve;
}; };*/
messenger.getChannelHead = function (curvePublic, cb) { /*messenger.getChannelHead = function (id, cb) {
var friend = friends[curvePublic]; var channel = getChannel(id);
if (channel.isFriendChat) {
var friend;
for (var k in friends) {
if (friends[k].channel === id) {
friend = friends[k];
break;
}
}
if (!friend) { return void cb('NO_SUCH_FRIEND'); } if (!friend) { return void cb('NO_SUCH_FRIEND'); }
cb(void 0, friend.lastKnownHash); cb(void 0, friend.lastKnownHash);
}; } else {
// TODO room
cb('NOT_IMPLEMENTED');
}
};*/
messenger.setChannelHead = function (curvePublic, hash, cb) { messenger.setChannelHead = function (id, hash, cb) {
var friend = friends[curvePublic]; var channel = getChannel(id);
if (channel.isFriendChat) {
var friend = getFriendFromChannel(id);
if (!friend) { return void cb('NO_SUCH_FRIEND'); } if (!friend) { return void cb('NO_SUCH_FRIEND'); }
friend.lastKnownHash = hash; friend.lastKnownHash = hash;
} else if (channel.isPadChat) {
// Nothing to do
} else {
// TODO room
return void cb('NOT_IMPLEMENTED');
}
cb(); cb();
}; };
// Id message allows us to map a netfluxId with a public curve key // Make sure the data we have about our friends are up-to-date when we see them online
var onIdMessage = function (msg, sender) { var checkFriendData = function (curve, data, channel) {
var channel; if (curve === proxy.curvePublic) { return; }
var isId = Object.keys(channels).some(function (chanId) { var friend = getFriend(proxy, curve);
if (channels[chanId].userList.indexOf(sender) !== -1) { if (!friend) { return; }
channel = channels[chanId]; var types = [];
return true; Object.keys(data).forEach(function (k) {
if (friend[k] !== data[k]) {
types.push(k);
friend[k] = data[k];
} }
}); });
if (!isId) { return; } eachHandler('update', function (f) {
f(clone(data), types, channel);
});
};
var decryptedMsg = channel.encryptor.decrypt(msg); // Id message allows us to map a netfluxId with a public curve key
var onIdMessage = function (msg, sender) {
var channel, parsed0;
try {
parsed0 = JSON.parse(msg);
channel = channels[parsed0.channel];
if (!channel) { return; }
if (channel.userList.indexOf(sender) === -1) { return; }
} catch (e) {
console.log(msg);
console.error(e);
// Not an ID message
return;
}
var decryptedMsg = channel.encryptor.decrypt(parsed0.msg);
if (decryptedMsg === null) { if (decryptedMsg === null) {
return void console.error("Failed to decrypt message"); return void console.error("Failed to decrypt message");
@ -206,20 +261,26 @@ define([
// the sender field. This is to prevent replay attacks. // the sender field. This is to prevent replay attacks.
if (parsed[2] !== sender || !parsed[1]) { return; } if (parsed[2] !== sender || !parsed[1]) { return; }
channel.mapId[sender] = parsed[1]; channel.mapId[sender] = parsed[1];
checkFriendData(parsed[1].curvePublic, parsed[1], channel.id);
eachHandler('join', function (f) { eachHandler('join', function (f) {
f(parsed[1], channel.id); f(parsed[1], channel.id);
}); });
if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK
// Answer with your own key // Answer with your own key
var rMsg = [Types.mapIdAck, proxy.curvePublic, channel.wc.myID]; var myData = createData(proxy);
delete myData.channel;
var rMsg = [Types.mapIdAck, myData, channel.wc.myID];
var rMsgStr = JSON.stringify(rMsg); var rMsgStr = JSON.stringify(rMsg);
var cryptMsg = channel.encryptor.encrypt(rMsgStr); var cryptMsg = channel.encryptor.encrypt(rMsgStr);
network.sendto(sender, cryptMsg); var data = {
channel: channel.id,
msg: cryptMsg
};
network.sendto(sender, JSON.stringify(data));
}; };
var orderMessages = function (curvePublic, new_messages /*, sig */) { var orderMessages = function (channel, new_messages) {
var channel = getChannel(curvePublic);
var messages = channel.messages; var messages = channel.messages;
// TODO improve performance, guarantee correct ordering // TODO improve performance, guarantee correct ordering
@ -236,9 +297,9 @@ define([
}; };
var pushMsg = function (channel, cryptMsg) { var pushMsg = function (channel, cryptMsg) {
var msg = channel.encryptor.decrypt(cryptMsg);
var sig = cryptMsg.slice(0, 64); var sig = cryptMsg.slice(0, 64);
if (msgAlreadyKnown(channel, sig)) { return; } if (msgAlreadyKnown(channel, sig)) { return; }
var msg = channel.encryptor.decrypt(cryptMsg);
var parsedMsg = JSON.parse(msg); var parsedMsg = JSON.parse(msg);
var curvePublic; var curvePublic;
@ -250,43 +311,38 @@ define([
author: parsedMsg[1], author: parsedMsg[1],
time: parsedMsg[2], time: parsedMsg[2],
text: parsedMsg[3], text: parsedMsg[3],
channel: channel.id,
name: parsedMsg[4] // Display name for multi-user rooms
// this makes debugging a whole lot easier // this makes debugging a whole lot easier
curve: getCurveForChannel(channel.id), //curve: getCurveForChannel(channel.id),
}; };
channel.messages.push(res); channel.messages.push(res);
if (!joining[channel.id]) {
// Channel is ready
eachHandler('message', function (f) { eachHandler('message', function (f) {
f(res); f(res);
}); });
}
return true; return true;
} }
if (parsedMsg[0] === Types.update) { if (parsedMsg[0] === Types.update) {
if (parsedMsg[1] === proxy.curvePublic) { return; } checkFriendData(parsedMsg[1], parsedMsg[3], channel.id);
curvePublic = parsedMsg[1];
var newdata = parsedMsg[3];
var data = getFriend(proxy, parsedMsg[1]);
var types = [];
Object.keys(newdata).forEach(function (k) {
if (data[k] !== newdata[k]) {
types.push(k);
data[k] = newdata[k];
}
});
eachHandler('update', function (f) {
f(clone(newdata), curvePublic);
});
return; return;
} }
if (parsedMsg[0] === Types.unfriend) { if (parsedMsg[0] === Types.unfriend) {
curvePublic = parsedMsg[1]; curvePublic = parsedMsg[1];
delete friends[curvePublic];
removeFromFriendList(parsedMsg[1], function () { // If this a removal from our part by in another tab, do nothing.
// The channel is already closed in the proxy.on('remove') part
if (curvePublic === proxy.curvePublic) { return; }
removeFromFriendList(curvePublic, function () {
channel.wc.leave(Types.unfriend); channel.wc.leave(Types.unfriend);
delete channels[channel.id];
eachHandler('unfriend', function (f) { eachHandler('unfriend', function (f) {
f(curvePublic); f(curvePublic, false);
}); });
}); });
return; return;
@ -324,7 +380,7 @@ define([
}); });
}); });
eachHandler('update', function (f) { eachHandler('update', function (f) {
f(myData, myData.curvePublic); f(myData, ['displayName', 'profile', 'avatar']);
}); });
friends.me = myData; friends.me = myData;
} }
@ -352,12 +408,26 @@ define([
return void console.error("received response to unknown request"); return void console.error("received response to unknown request");
} }
if (!req.cb) {
// This is the initial history for a pad chat
if (type === 'HISTORY_RANGE') {
if (!getChannel(req.chanId)) { return; }
if (!Array.isArray(parsed[2])) { return; }
pushMsg(getChannel(req.chanId), parsed[2][4]);
} else if (type === 'HISTORY_RANGE_END') {
if (!getChannel(req.chanId)) { return; }
getChannel(req.chanId).ready = true;
onChannelReady(req.chanId);
return;
}
return;
}
if (type === 'HISTORY_RANGE') { if (type === 'HISTORY_RANGE') {
req.messages.push(parsed[2]); req.messages.push(parsed[2]);
} else if (type === 'HISTORY_RANGE_END') { } else if (type === 'HISTORY_RANGE_END') {
// process all the messages (decrypt) // process all the messages (decrypt)
var curvePublic = req.curvePublic; var channel = getChannel(req.chanId);
var channel = getChannel(curvePublic);
var decrypted = req.messages.map(function (msg) { var decrypted = req.messages.map(function (msg) {
if (msg[2] !== 'MSG') { return; } if (msg[2] !== 'MSG') { return; }
@ -371,6 +441,8 @@ define([
return null; return null;
} }
}).filter(function (decrypted) { }).filter(function (decrypted) {
if (!decrypted.d || decrypted.d[0] !== Types.message) { return; }
if (msgAlreadyKnown(channel, decrypted.sig)) { return; }
return decrypted; return decrypted;
}).map(function (O) { }).map(function (O) {
return { return {
@ -379,11 +451,12 @@ define([
author: O.d[1], author: O.d[1],
time: O.d[2], time: O.d[2],
text: O.d[3], text: O.d[3],
curve: curvePublic, channel: req.chanId,
name: O.d[4]
}; };
}); });
orderMessages(curvePublic, decrypted, req.sig); orderMessages(channel, decrypted);
req.cb(void 0, decrypted); req.cb(void 0, decrypted);
return deleteRangeRequest(txid); return deleteRangeRequest(txid);
} else { } else {
@ -395,20 +468,17 @@ define([
if ((parsed.validateKey || parsed.owners) && parsed.channel) { if ((parsed.validateKey || parsed.owners) && parsed.channel) {
return; return;
} }
// End of initial history
if (parsed.state && parsed.state === 1 && parsed.channel) { if (parsed.state && parsed.state === 1 && parsed.channel) {
if (channels[parsed.channel]) { if (channels[parsed.channel]) {
// parsed.channel is Ready // parsed.channel is Ready
// channel[parsed.channel].ready(); // channel[parsed.channel].ready();
channels[parsed.channel].ready = true; channels[parsed.channel].ready = true;
onChannelReady(parsed.channel); onChannelReady(parsed.channel);
var updateTypes = channels[parsed.channel].updateOnReady;
if (updateTypes) {
//channels[parsed.channel].updateUI(updateTypes);
}
} }
return; return;
} }
// Initial history message
var chan = parsed[3]; var chan = parsed[3];
if (!chan || !channels[chan]) { return; } if (!chan || !channels[chan]) { return; }
pushMsg(channels[chan], parsed[4]); pushMsg(channels[chan], parsed[4]);
@ -440,7 +510,7 @@ define([
if (!data) { if (!data) {
// friend is not valid // friend is not valid
console.error('friend is not valid'); console.error('friend is not valid');
return; return void cb('INVALID_FRIEND');
} }
var channel = channels[data.channel]; var channel = channels[data.channel];
@ -458,12 +528,13 @@ define([
var msgStr = JSON.stringify(msg); var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encryptor.encrypt(msgStr); var cryptMsg = channel.encryptor.encrypt(msgStr);
// TODO emit remove_friend event?
try { try {
channel.wc.bcast(cryptMsg).then(function () { channel.wc.bcast(cryptMsg).then(function () {
delete friends[curvePublic]; removeFromFriendList(curvePublic, function () {
delete channels[curvePublic]; delete channels[channel.id];
Realtime.whenRealtimeSyncs(realtime, function () { eachHandler('unfriend', function (f) {
f(curvePublic, true);
});
cb(); cb();
}); });
}, function (err) { }, function (err) {
@ -476,9 +547,27 @@ define([
}; };
var getChannelMessagesSince = function (chan, data, keys) { var getChannelMessagesSince = function (chan, data, keys) {
console.log('Fetching [%s] messages since [%s]', data.curvePublic, data.lastKnownHash || ''); console.log('Fetching [%s] messages since [%s]', chan.id, data.lastKnownHash || '');
if (chan.isPadChat) {
// We need to use GET_HISTORY_RANGE to make sure we won't get the full history
var txid = Util.uid();
initRangeRequest(txid, chan.id, undefined);
var msg0 = ['GET_HISTORY_RANGE', chan.id, {
//from: hash,
count: 10,
txid: txid,
}
];
network.sendto(network.historyKeeper, JSON.stringify(msg0)).then(function () {
}, function (err) {
throw new Error(err);
});
return;
}
var cfg = { var cfg = {
validateKey: keys.validateKey, validateKey: keys ? keys.validateKey : undefined,
owners: [proxy.edPublic, data.edPublic], owners: [proxy.edPublic, data.edPublic],
lastKnownHash: data.lastKnownHash lastKnownHash: data.lastKnownHash
}; };
@ -489,79 +578,88 @@ define([
}); });
}; };
var openFriendChannel = function (data, f) { var openChannel = function (data) {
var keys = Curve.deriveKeys(data.curvePublic, proxy.curvePrivate); var keys = data.keys;
var encryptor = Curve.createEncryptor(keys); var encryptor = data.encryptor || Curve.createEncryptor(keys);
network.join(data.channel).then(function (chan) { var channel = {
var channel = channels[data.channel] = {
id: data.channel, id: data.channel,
isFriendChat: data.isFriendChat,
isPadChat: data.isPadChat,
sending: false, sending: false,
friendEd: f,
keys: keys,
curve: data.curvePublic,
encryptor: encryptor, encryptor: encryptor,
messages: [], messages: [],
wc: chan,
userList: [], userList: [],
mapId: {}, mapId: {},
send: function (payload, cb) {
if (!network.webChannels.some(function (wc) {
if (wc.id === channel.wc.id) { return true; }
})) {
return void cb('NO_SUCH_CHANNEL');
}
var msg = [Types.message, proxy.curvePublic, +new Date(), payload];
var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encryptor.encrypt(msgStr);
channel.wc.bcast(cryptMsg).then(function () {
pushMsg(channel, cryptMsg);
cb();
}, function (err) {
cb(err);
});
}
}; };
chan.on('message', function (msg, sender) {
onMessage(msg, sender, chan);
});
var onJoining = function (peer) { var onJoining = function (peer) {
if (peer === Msg.hk) { return; } if (peer === Msg.hk) { return; }
if (channel.userList.indexOf(peer) !== -1) { return; } if (channel.userList.indexOf(peer) !== -1) { return; }
channel.userList.push(peer); channel.userList.push(peer);
var msg = [Types.mapId, proxy.curvePublic, chan.myID];
// Join event will be sent once we are able to ID this peer
var myData = createData(proxy);
delete myData.channel;
var msg = [Types.mapId, myData, channel.wc.myID];
var msgStr = JSON.stringify(msg); var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encryptor.encrypt(msgStr); var cryptMsg = channel.encryptor.encrypt(msgStr);
network.sendto(peer, cryptMsg); var data = {
channel: channel.id,
msg: cryptMsg
}; };
chan.members.forEach(function (peer) { network.sendto(peer, JSON.stringify(data));
if (peer === Msg.hk) { return; } };
if (channel.userList.indexOf(peer) !== -1) { return; }
channel.userList.push(peer); var onLeaving = function (peer) {
});
chan.on('join', onJoining);
chan.on('leave', function (peer) {
var curvePublic = channel.mapId[peer];
var i = channel.userList.indexOf(peer); var i = channel.userList.indexOf(peer);
while (i !== -1) { while (i !== -1) {
channel.userList.splice(i, 1); channel.userList.splice(i, 1);
i = channel.userList.indexOf(peer); i = channel.userList.indexOf(peer);
} }
// update status // update status
if (!curvePublic) { return; } var otherData = channel.mapId[peer];
if (!otherData) { return; }
// Make sure the leaving user is not connected with another netflux id
if (channel.userList.some(function (nId) {
return channel.mapId[nId]
&& channel.mapId[nId].curvePublic === otherData.curvePublic;
})) { return; }
// Send the notification
eachHandler('leave', function (f) { eachHandler('leave', function (f) {
f(curvePublic, channel.id); f(otherData, channel.id);
});
};
var onOpen = function (chan) {
channel.wc = chan;
channels[data.channel] = channel;
chan.on('message', function (msg, sender) {
onMessage(msg, sender, chan);
}); });
chan.members.forEach(function (peer) {
if (peer === Msg.hk) { return; }
if (channel.userList.indexOf(peer) !== -1) { return; }
channel.userList.push(peer);
}); });
chan.on('join', onJoining);
chan.on('leave', onLeaving);
// FIXME don't subscribe to the channel implicitly // FIXME don't subscribe to the channel implicitly
getChannelMessagesSince(chan, data, keys); getChannelMessagesSince(channel, data, keys);
}, function (err) { };
network.join(data.channel).then(onOpen, function (err) {
console.error(err); console.error(err);
}); });
network.on('reconnect', function () {
if (!channels[data.channel]) { return; }
network.join(data.channel).then(onOpen, function (err) {
console.error(err);
});
});
}; };
messenger.getFriendList = function (cb) { messenger.getFriendList = function (cb) {
@ -573,7 +671,7 @@ define([
})); }));
}; };
messenger.openFriendChannel = function (curvePublic, cb) { /*messenger.openFriendChannel = function (curvePublic, cb) {
if (typeof(curvePublic) !== 'string') { return void cb('INVALID_ID'); } if (typeof(curvePublic) !== 'string') { return void cb('INVALID_ID'); }
if (typeof(cb) !== 'function') { throw new Error('expected callback'); } if (typeof(cb) !== 'function') { throw new Error('expected callback'); }
@ -585,10 +683,10 @@ define([
if (!channel) { return void cb('E_NO_CHANNEL'); } if (!channel) { return void cb('E_NO_CHANNEL'); }
joining[channel] = cb; joining[channel] = cb;
openFriendChannel(friend, curvePublic); openFriendChannel(friend, curvePublic);
}; };*/
messenger.sendMessage = function (curvePublic, payload, cb) { messenger.sendMessage = function (id, payload, cb) {
var channel = getChannel(curvePublic); var channel = getChannel(id);
if (!channel) { return void cb('NO_CHANNEL'); } if (!channel) { return void cb('NO_CHANNEL'); }
if (!network.webChannels.some(function (wc) { if (!network.webChannels.some(function (wc) {
if (wc.id === channel.wc.id) { return true; } if (wc.id === channel.wc.id) { return true; }
@ -597,6 +695,9 @@ define([
} }
var msg = [Types.message, proxy.curvePublic, +new Date(), payload]; var msg = [Types.message, proxy.curvePublic, +new Date(), payload];
if (!channel.isFriendChat) {
msg.push(proxy[Constants.displayNameKey]);
}
var msgStr = JSON.stringify(msg); var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encryptor.encrypt(msgStr); var cryptMsg = channel.encryptor.encrypt(msgStr);
@ -608,18 +709,27 @@ define([
}); });
}; };
messenger.getStatus = function (curvePublic, cb) { messenger.getStatus = function (chanId, cb) {
var channel = getChannel(curvePublic); // Display green status if one member is not me
var channel = getChannel(chanId);
if (!channel) { return void cb('NO_SUCH_CHANNEL'); } if (!channel) { return void cb('NO_SUCH_CHANNEL'); }
var online = channel.userList.some(function (nId) { var online = channel.userList.some(function (nId) {
return channel.mapId[nId] === curvePublic; var data = channel.mapId[nId] || undefined;
if (!data) { return false; }
return data.curvePublic !== proxy.curvePublic;
}); });
cb(void 0, online); cb(void 0, online);
}; };
messenger.getFriendInfo = function (curvePublic, cb) { messenger.getFriendInfo = function (channel, cb) {
setTimeout(function () { setTimeout(function () {
var friend = friends[curvePublic]; var friend;
for (var k in friends) {
if (friends[k].channel === channel) {
friend = friends[k];
break;
}
}
if (!friend) { return void cb('NO_SUCH_FRIEND'); } if (!friend) { return void cb('NO_SUCH_FRIEND'); }
// this clone will be redundant when ui uses postmessage // this clone will be redundant when ui uses postmessage
cb(void 0, clone(friend)); cb(void 0, clone(friend));
@ -633,28 +743,215 @@ define([
}); });
}; };
// TODO listen for changes to your friend list var loadFriend = function (friend, cb) {
// emit 'update' events for clients var channel = friend.channel;
if (getChannel(channel)) { return void cb(); }
joining[channel] = cb;
var keys = Curve.deriveKeys(friend.curvePublic, proxy.curvePrivate);
var data = {
keys: keys,
channel: friend.channel,
lastKnownHash: friend.lastKnownHash,
owners: [proxy.edPublic, friend.edPublic],
isFriendChat: true
};
openChannel(data);
};
//var update = function (curvePublic // Detect friends changes made in another worker
proxy.on('change', ['friends'], function (o, n, p) { proxy.on('change', ['friends'], function (o, n, p) {
var curvePublic; var curvePublic;
if (o === undefined) { if (o === undefined) {
// new friend added // new friend added
curvePublic = p.slice(-1)[0]; curvePublic = p.slice(-1)[0];
// Load channel
var friend = friends[curvePublic];
if (typeof(friend) !== 'object') { return; }
var channel = friend.channel;
if (!channel) { return; }
loadFriend(friend, function () {
eachHandler('friend', function (f) { eachHandler('friend', function (f) {
f(curvePublic, clone(n)); f(curvePublic);
});
}); });
return; return;
} }
console.error(o, n, p); if (typeof(n) === 'undefined') {
// Handled by .on('remove')
return;
}
}).on('remove', ['friends'], function (o, p) { }).on('remove', ['friends'], function (o, p) {
var curvePublic = p[1];
if (!curvePublic) { return; }
if (p[2] !== 'channel') { return; }
var channel = channels[o];
channel.wc.leave(Types.unfriend);
delete channels[channel.id];
eachHandler('unfriend', function (f) { eachHandler('unfriend', function (f) {
f(p[1]); // TODO f(curvePublic, true);
}); });
}); });
// Friend added in our contacts in the current worker
messenger.onFriendAdded = function (friendData) {
var friend = friends[friendData.curvePublic];
if (typeof(friend) !== 'object') { return; }
var channel = friend.channel;
if (!channel) { return; }
loadFriend(friend, function () {
eachHandler('friend', function (f) {
f(friend.curvePublic);
});
});
};
var ready = false;
var initialized = false;
var init = function () {
if (initialized) { return; }
initialized = true;
var friends = getFriendList(proxy);
nThen(function (waitFor) {
Object.keys(friends).forEach(function (key) {
if (key === 'me') { return; }
var friend = clone(friends[key]);
if (typeof(friend) !== 'object') { return; }
var channel = friend.channel;
if (!channel) { return; }
loadFriend(friend, waitFor());
});
// TODO load rooms
}).nThen(function () {
ready = true;
emit('READY');
});
};
//init();
var getRooms = function (data, cb) {
if (data && data.curvePublic) {
var curvePublic = data.curvePublic;
// We need to get data about a new friend's room
var friend = getFriend(proxy, curvePublic);
if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); }
var channel = getChannel(friend.channel);
if (!channel) { return void cb({error: 'NO_SUCH_CHANNEL'}); }
return void cb([{
id: channel.id,
isFriendChat: true,
name: friend.displayName,
lastKnownHash: friend.lastKnownHash,
curvePublic: friend.curvePublic,
messages: channel.messages
}]);
}
if (data && data.padChat) {
var pCChannel = getChannel(data.padChat);
if (!pCChannel) { return void cb({error: 'NO_SUCH_CHANNEL'}); }
return void cb([{
id: pCChannel.id,
isPadChat: true,
messages: pCChannel.messages
}]);
}
var rooms = Object.keys(channels).map(function (id) {
var r = getChannel(id);
var name, lastKnownHash, curvePublic;
if (r.isFriendChat) {
var friend = getFriendFromChannel(id);
if (!friend) { return null; }
name = friend.displayName;
lastKnownHash = friend.lastKnownHash;
curvePublic = friend.curvePublic;
} else if (r.isPadChat) {
return;
} else {
// TODO room get metadata (name) && lastKnownHash
}
return {
id: r.id,
isFriendChat: r.isFriendChat,
name: name,
lastKnownHash: lastKnownHash,
curvePublic: curvePublic,
messages: r.messages
};
}).filter(function (x) { return x; });
cb(rooms);
};
var getUserList = function (data, cb) {
var room = getChannel(data.id);
if (!room) { return void cb({error: 'NO_SUCH_CHANNEL'}); }
if (room.isFriendChat) {
var friend = getFriendFromChannel(data.id);
if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); }
cb([friend]);
} else {
// TODO room userlist in rooms...
// (this is the static userlist, not the netflux one)
cb([]);
}
};
var openPadChat = function (data, cb) {
var channel = data.channel;
if (getChannel(channel)) {
emit('PADCHAT_READY', channel);
return void cb();
}
var keys = data.secret && data.secret.keys;
var cryptKey = keys.viewKeyStr ? Crypto.b64AddSlashes(keys.viewKeyStr) : data.secret.key;
var encryptor = Crypto.createEncryptor(cryptKey);
var chanData = {
encryptor: encryptor,
channel: data.channel,
isPadChat: true,
//lastKnownHash: friend.lastKnownHash,
//owners: [proxy.edPublic, friend.edPublic],
//isFriendChat: true
};
openChannel(chanData);
joining[channel] = function () {
emit('PADCHAT_READY', channel);
};
cb();
};
network.on('disconnect', function () {
emit('DISCONNECT');
});
network.on('reconnect', function () {
emit('RECONNECT');
});
messenger.execCommand = function (obj, cb) {
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'INIT_FRIENDS') {
init();
return void cb();
}
if (cmd === 'IS_READY') {
return void cb(ready);
}
if (cmd === 'GET_ROOMS') {
return void getRooms(data, cb);
}
if (cmd === 'GET_USERLIST') {
return void getUserList(data, cb);
}
if (cmd === 'OPEN_PAD_CHAT') {
return void openPadChat(data, cb);
}
};
Object.freeze(messenger); Object.freeze(messenger);
return messenger; return messenger;

@ -12,10 +12,11 @@ define([
'/common/clipboard.js', '/common/clipboard.js',
'/customize/messages.js', '/customize/messages.js',
'/customize/application_config.js', '/customize/application_config.js',
'/customize/pages.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'css!/customize/fonts/cptools/style.css' 'css!/customize/fonts/cptools/style.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, NThen) { Messages, AppConfig, Pages, NThen) {
var UIElements = {}; var UIElements = {};
// Configure MediaTags to use our local viewer // Configure MediaTags to use our local viewer
@ -630,9 +631,11 @@ define([
if (!data.FM) { return; } if (!data.FM) { return; }
var $input = $('<input>', { var $input = $('<input>', {
'type': 'file', 'type': 'file',
'style': 'display: none;' 'style': 'display: none;',
'multiple': 'multiple'
}).on('change', function (e) { }).on('change', function (e) {
var file = e.target.files[0]; var files = Util.slice(e.target.files);
files.forEach(function (file) {
var ev = { var ev = {
target: data.target target: data.target
}; };
@ -642,11 +645,11 @@ define([
if (data.transformer) { if (data.transformer) {
data.transformer(file, function (newFile) { data.transformer(file, function (newFile) {
data.FM.handleFile(newFile, ev); data.FM.handleFile(newFile, ev);
if (callback) { callback(); }
}); });
return; return;
} }
data.FM.handleFile(file, ev); data.FM.handleFile(file, ev);
});
if (callback) { callback(); } if (callback) { callback(); }
}); });
if (data.accept) { $input.attr('accept', data.accept); } if (data.accept) { $input.attr('accept', data.accept); }
@ -1792,13 +1795,16 @@ define([
var $container = $('<div>'); var $container = $('<div>');
var i = 0; var i = 0;
AppConfig.availablePadTypes.forEach(function (p) { var types = AppConfig.availablePadTypes.filter(function (p) {
if (p === 'drive') { return; } if (p === 'drive') { return; }
if (p === 'contacts') { return; } if (p === 'contacts') { return; }
if (p === 'todo') { return; } if (p === 'todo') { return; }
if (p === 'file') { return; } if (p === 'file') { return; }
if (!common.isLoggedIn() && AppConfig.registeredOnlyTypes && if (!common.isLoggedIn() && AppConfig.registeredOnlyTypes &&
AppConfig.registeredOnlyTypes.indexOf(p) !== -1) { return; } AppConfig.registeredOnlyTypes.indexOf(p) !== -1) { return; }
return true;
});
types.forEach(function (p) {
var $element = $('<li>', { var $element = $('<li>', {
'class': 'cp-icons-element', 'class': 'cp-icons-element',
'id': 'cp-newpad-icons-'+ (i++) 'id': 'cp-newpad-icons-'+ (i++)
@ -1822,7 +1828,7 @@ define([
var selected = -1; var selected = -1;
var next = function () { var next = function () {
selected = ++selected % 5; selected = ++selected % types.length;
$container.find('.cp-icons-element-selected').removeClass('cp-icons-element-selected'); $container.find('.cp-icons-element-selected').removeClass('cp-icons-element-selected');
$container.find('#cp-newpad-icons-'+selected).addClass('cp-icons-element-selected'); $container.find('#cp-newpad-icons-'+selected).addClass('cp-icons-element-selected');
}; };
@ -2338,10 +2344,57 @@ define([
$(password).find('.cp-password-input').focus(); $(password).find('.cp-password-input').focus();
}; };
var crowdfundingState = false;
UIElements.displayCrowdfunding = function (common) {
if (crowdfundingState) { return; }
if (AppConfig.disableCrowdfundingMessages) { return; }
var priv = common.getMetadataMgr().getPrivateData();
if (priv.plan) { return; }
crowdfundingState = true;
setTimeout(function () {
common.getAttribute(['general', 'crowdfunding'], function (err, val) {
if (err || val === false) { return; }
// Display the popup
var text = Messages.crowdfunding_popup_text;
var yes = h('button.cp-corner-primary', Messages.crowdfunding_popup_yes);
var no = h('button.cp-corner-primary', Messages.crowdfunding_popup_no);
var never = h('button.cp-corner-cancel', Messages.crowdfunding_popup_never);
var actions = h('div', [yes, no, never]);
var modal = UI.cornerPopup(text, actions, null, {big: true});
$(yes).click(function () {
modal.delete();
common.openURL('https://opencollective.com/cryptpad/contribute');
Feedback.send('CROWDFUNDING_YES');
});
$(modal.popup).find('a').click(function (e) {
e.stopPropagation();
e.preventDefault();
modal.delete();
common.openURL('https://opencollective.com/cryptpad/');
Feedback.send('CROWDFUNDING_LINK');
});
$(no).click(function () {
modal.delete();
Feedback.send('CROWDFUNDING_NO');
});
$(never).click(function () {
modal.delete();
common.setAttribute(['general', 'crowdfunding'], false);
Feedback.send('CROWDFUNDING_NEVER');
});
});
}, 5000);
};
var storePopupState = false; var storePopupState = false;
UIElements.displayStorePadPopup = function (common, data) { UIElements.displayStorePadPopup = function (common, data) {
if (storePopupState) { return; } if (storePopupState) { return; }
storePopupState = true; storePopupState = true;
if (data && data.stored) { return; } // We won't display the popup for dropped files
var text = Messages.autostore_notstored; var text = Messages.autostore_notstored;
var footer = Messages.autostore_settings; var footer = Messages.autostore_settings;
@ -2359,15 +2412,20 @@ define([
}); });
$(hide).click(function () { $(hide).click(function () {
UIElements.displayCrowdfunding(common);
modal.delete(); modal.delete();
}); });
$(store).click(function () { $(store).click(function () {
modal.delete();
common.getSframeChannel().query("Q_AUTOSTORE_STORE", null, function (err, obj) { common.getSframeChannel().query("Q_AUTOSTORE_STORE", null, function (err, obj) {
if (err || (obj && obj.error)) { var error = err || (obj && obj.error);
console.error(err || obj.error); if (error) {
if (error === 'E_OVER_LIMIT') {
return void UI.warn(Messages.pinLimitReached);
}
return void UI.warn(Messages.autostore_error); return void UI.warn(Messages.autostore_error);
} }
modal.delete();
UIElements.displayCrowdfunding(common);
UI.log(Messages.autostore_saved); UI.log(Messages.autostore_saved);
}); });
}); });

@ -345,6 +345,9 @@ define([
}; };
common.getPadAttribute = function (attr, cb, href) { common.getPadAttribute = function (attr, cb, href) {
href = Hash.getRelativeHref(href || window.location.href); href = Hash.getRelativeHref(href || window.location.href);
if (!href) {
return void cb('E404');
}
postMessage("GET_PAD_ATTRIBUTE", { postMessage("GET_PAD_ATTRIBUTE", {
href: href, href: href,
attr: attr, attr: attr,
@ -622,6 +625,12 @@ define([
messenger.setChannelHead = function (data, cb) { messenger.setChannelHead = function (data, cb) {
postMessage("CONTACTS_SET_CHANNEL_HEAD", data, cb); postMessage("CONTACTS_SET_CHANNEL_HEAD", data, cb);
}; };
messenger.execCommand = function (data, cb) {
postMessage("CHAT_COMMAND", data, cb);
};
messenger.onEvent = Util.mkEvent();
messenger.onMessageEvent = Util.mkEvent(); messenger.onMessageEvent = Util.mkEvent();
messenger.onJoinEvent = Util.mkEvent(); messenger.onJoinEvent = Util.mkEvent();
messenger.onLeaveEvent = Util.mkEvent(); messenger.onLeaveEvent = Util.mkEvent();
@ -1059,6 +1068,8 @@ define([
CONTACTS_UPDATE: common.messenger.onUpdateEvent.fire, CONTACTS_UPDATE: common.messenger.onUpdateEvent.fire,
CONTACTS_FRIEND: common.messenger.onFriendEvent.fire, CONTACTS_FRIEND: common.messenger.onFriendEvent.fire,
CONTACTS_UNFRIEND: common.messenger.onUnfriendEvent.fire, CONTACTS_UNFRIEND: common.messenger.onUnfriendEvent.fire,
// Chat
CHAT_EVENT: common.messenger.onEvent.fire,
// Pad // Pad
PAD_READY: common.padRpc.onReadyEvent.fire, PAD_READY: common.padRpc.onReadyEvent.fire,
PAD_MESSAGE: common.padRpc.onMessageEvent.fire, PAD_MESSAGE: common.padRpc.onMessageEvent.fire,
@ -1425,7 +1436,7 @@ define([
postMessage("INIT_RPC", null, waitFor(function (obj) { postMessage("INIT_RPC", null, waitFor(function (obj) {
console.log('RPC handshake complete'); console.log('RPC handshake complete');
if (obj.error) { return; } if (obj.error) { return; }
localStorage.plan = obj.plan; localStorage[Constants.plan] = obj.plan;
})); }));
} else if (PINNING_ENABLED) { } else if (PINNING_ENABLED) {
console.log('not logged in. pads will not be pinned'); console.log('not logged in. pads will not be pinned');

@ -4,16 +4,32 @@ define([
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-util.js', '/common/common-util.js',
'/common/media-tag.js', '/common/media-tag.js',
'/common/highlight/highlight.pack.js',
'/bower_components/diff-dom/diffDOM.js', '/bower_components/diff-dom/diffDOM.js',
'/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/tweetnacl/nacl-fast.min.js',
],function ($, Marked, Hash, Util, MediaTag) { 'css!/common/highlight/styles/github.css'
],function ($, Marked, Hash, Util, MediaTag, Highlight) {
var DiffMd = {}; var DiffMd = {};
var DiffDOM = window.diffDOM; var DiffDOM = window.diffDOM;
var renderer = new Marked.Renderer(); var renderer = new Marked.Renderer();
var highlighter = function () {
return function(code, lang) {
if (lang) {
try {
return Highlight.highlight(lang, code).value;
} catch (e) {
return code;
}
}
return code;
};
};
Marked.setOptions({ Marked.setOptions({
renderer: renderer renderer: renderer,
highlight: highlighter(),
}); });
DiffMd.render = function (md) { DiffMd.render = function (md) {
@ -25,9 +41,11 @@ define([
// Tasks list // Tasks list
var checkedTaskItemPtn = /^\s*(<p>)?\[[xX]\](<\/p>)?\s*/; var checkedTaskItemPtn = /^\s*(<p>)?\[[xX]\](<\/p>)?\s*/;
var uncheckedTaskItemPtn = /^\s*(<p>)?\[ ?\](<\/p>)?\s*/; var uncheckedTaskItemPtn = /^\s*(<p>)?\[ ?\](<\/p>)?\s*/;
var bogusCheckPtn = /<input( checked=""){0,1} disabled="" type="checkbox">/;
renderer.listitem = function (text) { renderer.listitem = function (text) {
var isCheckedTaskItem = checkedTaskItemPtn.test(text); var isCheckedTaskItem = checkedTaskItemPtn.test(text);
var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text); var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text);
var hasBogusInput = bogusCheckPtn.test(text);
if (isCheckedTaskItem) { if (isCheckedTaskItem) {
text = text.replace(checkedTaskItemPtn, text = text.replace(checkedTaskItemPtn,
'<i class="fa fa-check-square" aria-hidden="true"></i>&nbsp;') + '\n'; '<i class="fa fa-check-square" aria-hidden="true"></i>&nbsp;') + '\n';
@ -36,6 +54,15 @@ define([
text = text.replace(uncheckedTaskItemPtn, text = text.replace(uncheckedTaskItemPtn,
'<i class="fa fa-square-o" aria-hidden="true"></i>&nbsp;') + '\n'; '<i class="fa fa-square-o" aria-hidden="true"></i>&nbsp;') + '\n';
} }
if (!isCheckedTaskItem && !isUncheckedTaskItem && hasBogusInput) {
if (/checked/.test(text)) {
text = text.replace(bogusCheckPtn,
'<i class="fa fa-check-square" aria-hidden="true"></i>&nbsp;') + '\n';
} else if (/disabled/.test(text)) {
text = text.replace(bogusCheckPtn,
'<i class="fa fa-square-o" aria-hidden="true"></i>&nbsp;') + '\n';
}
}
var cls = (isCheckedTaskItem || isUncheckedTaskItem) ? ' class="todo-list-item"' : ''; var cls = (isCheckedTaskItem || isUncheckedTaskItem) ? ' class="todo-list-item"' : '';
return '<li'+ cls + '>' + text + '</li>\n'; return '<li'+ cls + '>' + text + '</li>\n';
}; };

File diff suppressed because one or more lines are too long

@ -0,0 +1,99 @@
/*
Original highlight.js style (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #F0F0F0;
}
/* Base color: saturation 0; */
.hljs,
.hljs-subst {
color: #444;
}
.hljs-comment {
color: #888888;
}
.hljs-keyword,
.hljs-attribute,
.hljs-selector-tag,
.hljs-meta-keyword,
.hljs-doctag,
.hljs-name {
font-weight: bold;
}
/* User color: hue: 0 */
.hljs-type,
.hljs-string,
.hljs-number,
.hljs-selector-id,
.hljs-selector-class,
.hljs-quote,
.hljs-template-tag,
.hljs-deletion {
color: #880000;
}
.hljs-title,
.hljs-section {
color: #880000;
font-weight: bold;
}
.hljs-regexp,
.hljs-symbol,
.hljs-variable,
.hljs-template-variable,
.hljs-link,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #BC6060;
}
/* Language color: hue: 90; */
.hljs-literal {
color: #78A960;
}
.hljs-built_in,
.hljs-bullet,
.hljs-code,
.hljs-addition {
color: #397300;
}
/* Meta color: hue: 200 */
.hljs-meta {
color: #1f7199;
}
.hljs-meta-string {
color: #4d99bf;
}
/* Misc effects */
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

@ -0,0 +1,99 @@
/*
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #998;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080;
}
.hljs-string,
.hljs-doctag {
color: #d14;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900;
font-weight: bold;
}
.hljs-subst {
font-weight: normal;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
color: #009926;
}
.hljs-symbol,
.hljs-bullet {
color: #990073;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-meta {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

@ -59,6 +59,9 @@ define([
obj[key] = data.value; obj[key] = data.value;
} }
broadcast([clientId], "UPDATE_METADATA"); broadcast([clientId], "UPDATE_METADATA");
if (Array.isArray(path) && path[0] === 'profile' && store.messenger) {
store.messenger.updateMyData();
}
onSync(cb); onSync(cb);
}; };
@ -462,7 +465,7 @@ define([
if (data.password) { pad.password = data.password; } if (data.password) { pad.password = data.password; }
if (data.channel) { pad.channel = data.channel; } if (data.channel) { pad.channel = data.channel; }
store.manager.addPad(data.path, pad, function (e) { store.manager.addPad(data.path, pad, function (e) {
if (e) { return void cb({error: "Error while adding the pad:"+ e}); } if (e) { return void cb({error: e}); }
sendDriveEvent('DRIVE_CHANGE', { sendDriveEvent('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA] path: ['drive', UserObject.FILES_DATA]
}, clientId); }, clientId);
@ -597,6 +600,7 @@ define([
Store.setDisplayName = function (clientId, value, cb) { Store.setDisplayName = function (clientId, value, cb) {
store.proxy[Constants.displayNameKey] = value; store.proxy[Constants.displayNameKey] = value;
broadcast([clientId], "UPDATE_METADATA"); broadcast([clientId], "UPDATE_METADATA");
if (store.messenger) { store.messenger.updateMyData(); }
onSync(cb); onSync(cb);
}; };
@ -796,12 +800,20 @@ define([
password: data.password, password: data.password,
path: data.path path: data.path
}, cb); }, cb);
// Let inner know that dropped files shouldn't trigger the popup
postMessage(clientId, "AUTOSTORE_DISPLAY_POPUP", {
stored: true
});
return; return;
} }
} else { } else {
sendDriveEvent('DRIVE_CHANGE', { sendDriveEvent('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA] path: ['drive', UserObject.FILES_DATA]
}, clientId); }, clientId);
// Let inner know that dropped files shouldn't trigger the popup
postMessage(clientId, "AUTOSTORE_DISPLAY_POPUP", {
stored: true
});
} }
onSync(cb); onSync(cb);
}; };
@ -851,6 +863,9 @@ define([
}, },
pinPads: function (data, cb) { Store.pinPads(null, data, cb); }, pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
friendComplete: function (data) { friendComplete: function (data) {
if (data.friend && store.messenger && store.messenger.onFriendAdded) {
store.messenger.onFriendAdded(data.friend);
}
postMessage(clientId, "EV_FRIEND_COMPLETE", data); postMessage(clientId, "EV_FRIEND_COMPLETE", data);
}, },
friendRequest: function (data, cb) { friendRequest: function (data, cb) {
@ -884,6 +899,7 @@ define([
Store.messenger = { Store.messenger = {
getFriendList: function (clientId, data, cb) { getFriendList: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.getFriendList(function (e, keys) { store.messenger.getFriendList(function (e, keys) {
cb({ cb({
error: e, error: e,
@ -892,6 +908,7 @@ define([
}); });
}, },
getMyInfo: function (clientId, data, cb) { getMyInfo: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.getMyInfo(function (e, info) { store.messenger.getMyInfo(function (e, info) {
cb({ cb({
error: e, error: e,
@ -900,6 +917,7 @@ define([
}); });
}, },
getFriendInfo: function (clientId, data, cb) { getFriendInfo: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.getFriendInfo(data, function (e, info) { store.messenger.getFriendInfo(data, function (e, info) {
cb({ cb({
error: e, error: e,
@ -908,6 +926,7 @@ define([
}); });
}, },
removeFriend: function (clientId, data, cb) { removeFriend: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.removeFriend(data, function (e, info) { store.messenger.removeFriend(data, function (e, info) {
cb({ cb({
error: e, error: e,
@ -916,11 +935,13 @@ define([
}); });
}, },
openFriendChannel: function (clientId, data, cb) { openFriendChannel: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.openFriendChannel(data, function (e) { store.messenger.openFriendChannel(data, function (e) {
cb({ error: e, }); cb({ error: e, });
}); });
}, },
getFriendStatus: function (clientId, data, cb) { getFriendStatus: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.getStatus(data, function (e, online) { store.messenger.getStatus(data, function (e, online) {
cb({ cb({
error: e, error: e,
@ -929,6 +950,7 @@ define([
}); });
}, },
getMoreHistory: function (clientId, data, cb) { getMoreHistory: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.getMoreHistory(data.curvePublic, data.sig, data.count, function (e, history) { store.messenger.getMoreHistory(data.curvePublic, data.sig, data.count, function (e, history) {
cb({ cb({
error: e, error: e,
@ -937,6 +959,7 @@ define([
}); });
}, },
sendMessage: function (clientId, data, cb) { sendMessage: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.sendMessage(data.curvePublic, data.content, function (e) { store.messenger.sendMessage(data.curvePublic, data.content, function (e) {
cb({ cb({
error: e, error: e,
@ -944,11 +967,17 @@ define([
}); });
}, },
setChannelHead: function (clientId, data, cb) { setChannelHead: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.setChannelHead(data.curvePublic, data.sig, function (e) { store.messenger.setChannelHead(data.curvePublic, data.sig, function (e) {
cb({ cb({
error: e error: e
}); });
}); });
},
execCommand: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.execCommand(data, cb);
} }
}; };
@ -1309,7 +1338,6 @@ define([
} }
}; };
var messengerEventInit = false;
var sendMessengerEvent = function (q, data) { var sendMessengerEvent = function (q, data) {
messengerEventClients.forEach(function (cId) { messengerEventClients.forEach(function (cId) {
postMessage(cId, q, data); postMessage(cId, q, data);
@ -1319,7 +1347,9 @@ define([
if (messengerEventClients.indexOf(clientId) === -1) { if (messengerEventClients.indexOf(clientId) === -1) {
messengerEventClients.push(clientId); messengerEventClients.push(clientId);
} }
if (!messengerEventInit) { };
var loadMessenger = function () {
if (AppConfig.availablePadTypes.indexOf('contacts') === -1) { return; }
var messenger = store.messenger = Messenger.messenger(store); var messenger = store.messenger = Messenger.messenger(store);
messenger.on('message', function (message) { messenger.on('message', function (message) {
sendMessengerEvent('CONTACTS_MESSAGE', message); sendMessengerEvent('CONTACTS_MESSAGE', message);
@ -1336,10 +1366,11 @@ define([
channel: channel, channel: channel,
}); });
}); });
messenger.on('update', function (info, curvePublic) { messenger.on('update', function (info, types, channel) {
sendMessengerEvent('CONTACTS_UPDATE', { sendMessengerEvent('CONTACTS_UPDATE', {
curvePublic: curvePublic, types: types,
info: info, info: info,
channel: channel
}); });
}); });
messenger.on('friend', function (curvePublic) { messenger.on('friend', function (curvePublic) {
@ -1347,13 +1378,18 @@ define([
curvePublic: curvePublic, curvePublic: curvePublic,
}); });
}); });
messenger.on('unfriend', function (curvePublic) { messenger.on('unfriend', function (curvePublic, removedByMe) {
sendMessengerEvent('CONTACTS_UNFRIEND', { sendMessengerEvent('CONTACTS_UNFRIEND', {
curvePublic: curvePublic, curvePublic: curvePublic,
removedByMe: removedByMe
});
});
messenger.on('event', function (ev, data) {
sendMessengerEvent('CHAT_EVENT', {
ev: ev,
data: data
}); });
}); });
messengerEventInit = true;
}
}; };
@ -1442,6 +1478,7 @@ define([
}); });
userObject.fixFiles(); userObject.fixFiles();
loadSharedFolders(waitFor); loadSharedFolders(waitFor);
loadMessenger();
}).nThen(function () { }).nThen(function () {
var requestLogin = function () { var requestLogin = function () {
broadcast([], "REQUEST_LOGIN"); broadcast([], "REQUEST_LOGIN");

@ -69,6 +69,8 @@ define([
CONTACTS_GET_MORE_HISTORY: Store.messenger.getMoreHistory, CONTACTS_GET_MORE_HISTORY: Store.messenger.getMoreHistory,
CONTACTS_SEND_MESSAGE: Store.messenger.sendMessage, CONTACTS_SEND_MESSAGE: Store.messenger.sendMessage,
CONTACTS_SET_CHANNEL_HEAD: Store.messenger.setChannelHead, CONTACTS_SET_CHANNEL_HEAD: Store.messenger.setChannelHead,
// Chat
CHAT_COMMAND: Store.messenger.execCommand,
// Pad // Pad
SEND_PAD_MSG: Store.sendPadMsg, SEND_PAD_MSG: Store.sendPadMsg,
JOIN_PAD: Store.joinPad, JOIN_PAD: Store.joinPad,

@ -322,6 +322,8 @@ define([
if (!readOnly) { onLocal(); } if (!readOnly) { onLocal(); }
evOnReady.fire(newPad); evOnReady.fire(newPad);
common.openPadChat(onLocal);
UI.removeLoadingScreen(emitResize); UI.removeLoadingScreen(emitResize);
var privateDat = cpNfInner.metadataMgr.getPrivateData(); var privateDat = cpNfInner.metadataMgr.getPrivateData();
@ -559,6 +561,7 @@ define([
}, onLocal); }, onLocal);
var configTb = { var configTb = {
displayed: [ displayed: [
'chat',
'userlist', 'userlist',
'title', 'title',
'useradmin', 'useradmin',

@ -36,7 +36,8 @@ define([
window.addEventListener('message', onMsg); window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {
SFCommonO.start({ SFCommonO.start({
useCreationScreen: true useCreationScreen: true,
messaging: true
}); });
}); });
}); });

@ -262,6 +262,7 @@ define([
donateURL: Cryptpad.donateURL, donateURL: Cryptpad.donateURL,
upgradeURL: Cryptpad.upgradeURL upgradeURL: Cryptpad.upgradeURL
}, },
plan: localStorage[Utils.Constants.plan],
isNewFile: isNewFile, isNewFile: isNewFile,
isDeleted: isNewFile && window.location.hash.length > 0, isDeleted: isNewFile && window.location.hash.length > 0,
forceCreationScreen: forceCreationScreen, forceCreationScreen: forceCreationScreen,
@ -370,7 +371,7 @@ define([
forceSave: true forceSave: true
}; };
Cryptpad.setPadTitle(data, function (err) { Cryptpad.setPadTitle(data, function (err) {
cb(err); cb({error: err});
}); });
}); });
sframeChan.on('Q_IS_PAD_STORED', function (data, cb) { sframeChan.on('Q_IS_PAD_STORED', function (data, cb) {
@ -776,6 +777,22 @@ define([
Cryptpad.messenger.setChannelHead(opt, cb); Cryptpad.messenger.setChannelHead(opt, cb);
}); });
sframeChan.on('Q_CHAT_OPENPADCHAT', function (data, cb) {
Cryptpad.messenger.execCommand({
cmd: 'OPEN_PAD_CHAT',
data: {
channel: data,
secret: secret
}
}, cb);
});
sframeChan.on('Q_CHAT_COMMAND', function (data, cb) {
Cryptpad.messenger.execCommand(data, cb);
});
Cryptpad.messenger.onEvent.reg(function (data) {
sframeChan.event('EV_CHAT_EVENT', data);
});
Cryptpad.messenger.onMessageEvent.reg(function (data) { Cryptpad.messenger.onMessageEvent.reg(function (data) {
sframeChan.event('EV_CONTACTS_MESSAGE', data); sframeChan.event('EV_CONTACTS_MESSAGE', data);
}); });

@ -161,6 +161,24 @@ define([
}); });
}; };
// Chat
var padChatChannel;
funcs.getPadChat = function () {
return padChatChannel;
};
funcs.openPadChat = function (saveChanges) {
var md = JSON.parse(JSON.stringify(ctx.metadataMgr.getMetadata()));
var channel = md.chat || Hash.createChannelId();
if (!md.chat) {
md.chat = channel;
ctx.metadataMgr.updateMetadata(md);
setTimeout(saveChanges);
}
padChatChannel = channel;
ctx.sframeChan.query('Q_CHAT_OPENPADCHAT', channel, function (err, obj) {
if (err || (obj && obj.error)) { console.error(err || (obj && obj.error)); }
});
};
// CodeMirror // CodeMirror
funcs.initCodeMirrorApp = callWithCommon(CodeMirror.create); funcs.initCodeMirrorApp = callWithCommon(CodeMirror.create);
@ -517,6 +535,11 @@ define([
UI.alert(Messages.chrome68); UI.alert(Messages.chrome68);
}); });
funcs.isPadStored(function (err, val) {
if (err || !val) { return; }
UIElements.displayCrowdfunding(funcs);
});
ctx.sframeChan.ready(); ctx.sframeChan.ready();
cb(funcs); cb(funcs);
}); });

@ -35,7 +35,7 @@ define([], function () {
}); });
sFrameChan.on('EV_CONTACTS_UPDATE', function (data) { sFrameChan.on('EV_CONTACTS_UPDATE', function (data) {
_handlers.update.forEach(function (f) { _handlers.update.forEach(function (f) {
f(data.info, data.curvePublic); f(data.info, data.types, data.channel);
}); });
}); });
sFrameChan.on('EV_CONTACTS_FRIEND', function (data) { sFrameChan.on('EV_CONTACTS_FRIEND', function (data) {
@ -45,7 +45,7 @@ define([], function () {
}); });
sFrameChan.on('EV_CONTACTS_UNFRIEND', function (data) { sFrameChan.on('EV_CONTACTS_UNFRIEND', function (data) {
_handlers.unfriend.forEach(function (f) { _handlers.unfriend.forEach(function (f) {
f(data.curvePublic); f(data.curvePublic, data.removedByMe);
}); });
}); });

@ -171,6 +171,11 @@ define({
'Q_CONTACTS_SET_CHANNEL_HEAD': true, 'Q_CONTACTS_SET_CHANNEL_HEAD': true,
'Q_CONTACTS_CLEAR_OWNED_CHANNEL': true, 'Q_CONTACTS_CLEAR_OWNED_CHANNEL': true,
// Chat
'EV_CHAT_EVENT': true,
'Q_CHAT_COMMAND': true,
'Q_CHAT_OPENPADCHAT': true,
// Put one or more entries to the localStore which will go in localStorage. // Put one or more entries to the localStore which will go in localStorage.
'EV_LOCALSTORE_PUT': true, 'EV_LOCALSTORE_PUT': true,
// Put one entry in the parent sessionStorage // Put one entry in the parent sessionStorage
@ -218,6 +223,8 @@ define({
// Refresh the drive when the drive has changed ('change' or 'remove' events) // Refresh the drive when the drive has changed ('change' or 'remove' events)
'EV_DRIVE_CHANGE': true, 'EV_DRIVE_CHANGE': true,
'EV_DRIVE_REMOVE': true, 'EV_DRIVE_REMOVE': true,
// Set shared folder hash in the address bar
'EV_DRIVE_SET_HASH': true,
// Remove an owned pad from the server // Remove an owned pad from the server
'Q_REMOVE_OWNED_CHANNEL': true, 'Q_REMOVE_OWNED_CHANNEL': true,

@ -6,8 +6,11 @@ define([
'/common/common-interface.js', '/common/common-interface.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-feedback.js', '/common/common-feedback.js',
'/common/sframe-messenger-inner.js',
'/contacts/messenger-ui.js',
'/customize/messages.js', '/customize/messages.js',
], function ($, Config, ApiConfig, UIElements, UI, Hash, Feedback, Messages) { ], function ($, Config, ApiConfig, UIElements, UI, Hash, Feedback,
Messenger, MessengerUI, Messages) {
var Common; var Common;
var Bar = { var Bar = {
@ -231,16 +234,15 @@ define([
var name = data.name || Messages.anonymous; var name = data.name || Messages.anonymous;
var $span = $('<span>', {'class': 'cp-avatar'}); var $span = $('<span>', {'class': 'cp-avatar'});
var $rightCol = $('<span>', {'class': 'cp-toolbar-userlist-rightcol'}); var $rightCol = $('<span>', {'class': 'cp-toolbar-userlist-rightcol'});
var $nameSpan = $('<span>', {'class': 'cp-toolbar-userlist-name'}).text(name).appendTo($rightCol); var $nameSpan = $('<span>', {'class': 'cp-toolbar-userlist-name'}).appendTo($rightCol);
var isMe = data.uid === user.uid;
if (isMe && !priv.readOnly) {
$nameSpan.html('');
var $nameValue = $('<span>', { var $nameValue = $('<span>', {
'class': 'cp-toolbar-userlist-name-value' 'class': 'cp-toolbar-userlist-name-value'
}).text(name).appendTo($nameSpan); }).text(name).appendTo($nameSpan);
var isMe = data.uid === user.uid;
if (isMe && !priv.readOnly) {
if (!Config.disableProfile) { if (!Config.disableProfile) {
var $button = $('<button>', { var $button = $('<button>', {
'class': 'fa fa-pencil cp-toolbar-userlist-name-edit', 'class': 'fa fa-pencil cp-toolbar-userlist-button',
title: Messages.user_rename title: Messages.user_rename
}).appendTo($nameSpan); }).appendTo($nameSpan);
$button.hover(function (e) { e.preventDefault(); e.stopPropagation(); }); $button.hover(function (e) { e.preventDefault(); e.stopPropagation(); });
@ -296,16 +298,24 @@ define([
$('<span>', {'class': 'cp-toolbar-userlist-friend'}).text(Messages.userlist_pending) $('<span>', {'class': 'cp-toolbar-userlist-friend'}).text(Messages.userlist_pending)
.appendTo($rightCol); .appendTo($rightCol);
} else { } else {
$('<span>', { $('<button>', {
'class': 'fa fa-user-plus cp-toolbar-userlist-friend', 'class': 'fa fa-user-plus cp-toolbar-userlist-button',
'title': Messages._getKey('userlist_addAsFriendTitle', [ 'title': Messages._getKey('userlist_addAsFriendTitle', [
name name
]) ])
}).appendTo($rightCol).click(function (e) { }).appendTo($nameSpan).click(function (e) {
e.stopPropagation(); e.stopPropagation();
Common.sendFriendRequest(data.netfluxId); Common.sendFriendRequest(data.netfluxId);
}); });
} }
} else if (Common.isLoggedIn() && data.curvePublic && friends[data.curvePublic]) {
$('<button>', {
'class': 'fa fa-comments-o cp-toolbar-userlist-button',
'title': Messages.userlist_chat
}).appendTo($nameSpan).click(function (e) {
e.stopPropagation();
Common.openURL('/contacts/');
});
} }
if (data.profile) { if (data.profile) {
$span.addClass('cp-userlist-clickable'); $span.addClass('cp-userlist-clickable');
@ -410,6 +420,77 @@ define([
return $container; return $container;
}; };
var initChat = function (toolbar) {
var $container = $('<div>', {
id: 'cp-app-contacts-container',
'class': 'cp-app-contacts-inapp'
}).prependTo(toolbar.chatContent);
var sframeChan = Common.getSframeChannel();
var messenger = Messenger.create(sframeChan);
MessengerUI.create(messenger, $container, Common, toolbar);
};
var createChat = function (toolbar, config) {
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the chat");
}
if (Config.availablePadTypes.indexOf('contacts') === -1) { return; }
var $content = $('<div>', {'class': 'cp-toolbar-chat-drawer'});
$content.on('drop dragover', function (e) {
e.preventDefault();
e.stopPropagation();
});
var $closeIcon = $('<span>', {"class": "fa fa-window-close cp-toolbar-chat-drawer-close"}).appendTo($content);
//$('<h2>').text(Messages.users).appendTo($content);
//$('<p>', {'class': USERLIST_CLS}).appendTo($content);
toolbar.chatContent = $content;
var $container = $('<span>', {id: 'cp-toolbar-chat-drawer-open', title: Messages.chatButton});
var $button = $('<button>', {'class': 'fa fa-comments'}).appendTo($container);
$('<span>',{'class': 'cp-dropdown-button-title'}).appendTo($button);
toolbar.$leftside.prepend($container);
if (config.$contentContainer) {
config.$contentContainer.prepend($content);
}
var hide = function () {
$content.hide();
$button.removeClass('cp-toolbar-button-active');
config.$contentContainer.removeClass('cp-chat-visible');
};
var show = function () {
if (Bar.isEmbed) { $content.hide(); return; }
$content.show();
$button.addClass('cp-toolbar-button-active');
config.$contentContainer.addClass('cp-chat-visible');
$button.removeClass('cp-toolbar-notification');
};
$closeIcon.click(function () {
Common.setAttribute(['toolbar', 'chat-drawer'], false);
hide();
});
$button.click(function () {
var visible = $content.is(':visible');
if (visible) { hide(); }
else { show(); }
visible = !visible;
Common.setAttribute(['toolbar', 'chat-drawer'], visible);
});
show();
Common.getAttribute(['toolbar', 'chat-drawer'], function (err, val) {
if (val === false || ($(window).height() < 800 && $(window).width() < 800)) {
return void hide();
}
show();
});
initChat(toolbar, config);
return $container;
};
var createShare = function (toolbar, config) { var createShare = function (toolbar, config) {
if (!config.metadataMgr) { if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the userlist"); throw new Error("You must provide a `metadataMgr` to display the userlist");
@ -997,6 +1078,7 @@ define([
// Create the subelements // Create the subelements
var tb = {}; var tb = {};
tb['userlist'] = createUserList; tb['userlist'] = createUserList;
tb['chat'] = createChat;
tb['share'] = createShare; tb['share'] = createShare;
tb['fileshare'] = createFileShare; tb['fileshare'] = createFileShare;
tb['title'] = createTitle; tb['title'] = createTitle;

@ -1,5 +1,5 @@
@import (reference) '../../customize/src/less2/include/avatar.less';
@import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/messenger.less';
// body // body
&.cp-app-contacts { &.cp-app-contacts {
@ -9,238 +9,14 @@
@color: @colortheme_friends-color @color: @colortheme_friends-color
); );
@keyframes example {
0% {
background: rgba(0,0,0,0.1);
}
50% {
background: rgba(0,0,0,0.3);
}
100% {
background: rgba(0,0,0,0.1);
}
}
display: flex; display: flex;
flex-flow: column; flex-flow: column;
background-color: red !important;
@button-border: 2px;
@bg-color: @colortheme_friends-bg;
@color: @colortheme_friends-color;
#cp-app-contacts-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
min-height: 0;
&.ready {
background-size: cover;
background-position: center;
}
}
#cp-toolbar { #cp-toolbar {
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-friendlist { .messenger_main();
width: 350px;
height: 100%;
background-color: lighten(@bg-color, 10%);
overflow-y: auto;
.cp-app-contacts-friend {
background: rgba(0,0,0,0.1);
padding: 5px;
margin: 10px;
cursor: pointer;
position: relative;
.cp-app-contacts-right-col {
margin-left: 5px;
display: flex;
flex-flow: column;
}
&:hover {
background-color: rgba(0,0,0,0.3);
}
&.cp-app-contacts-notify {
animation: example 2s ease-in-out infinite;
}
}
.cp-app-contacts-remove {
cursor: pointer;
width: 20px;
&:hover {
color: darken(@color, 20%);
}
}
}
#cp-app-contacts-friendlist .cp-app-contacts-friend, #cp-app-contacts-messaging .cp-avatar {
.avatar_main(30px);
&.cp-avatar {
display: flex;
}
cursor: pointer;
color: @color;
media-tag {
img {
color: #000;
}
}
media-tag, .cp-avatar-default {
margin-right: 5px;
}
.cp-app-contacts-status {
width: 5px;
display: inline-block;
position: absolute;
right: 0;
top: 0;
bottom: 0;
opacity: 0.7;
background-color: #777;
&.cp-app-contacts-online {
background-color: green;
}
&.cp-app-contacts-offline {
background-color: red;
}
}
}
.placeholder (@color: #bbb) {
&::-webkit-input-placeholder { /* WebKit, Blink, Edge */
color: @color;
}
&:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
color: @color;
opacity: 1;
}
&::-moz-placeholder { /* Mozilla Firefox 19+ */
color: @color;
opacity: 1;
}
&:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: @color;
}
&::-ms-input-placeholder { /* Microsoft Edge */
color: @color;
}
}
#cp-app-contacts-messaging {
flex: 1;
height: 100%;
background-color: lighten(@bg-color, 20%);
min-width: 0;
.cp-app-contacts-info {
padding: 20px;
}
.cp-app-contacts-header {
background-color: lighten(@bg-color, 15%);
padding: 0;
display: flex;
justify-content: space-between;
align-items: center;
height: 50px;
.hover () {
height: 100%;
line-height: 30px;
padding: 10px;
&:hover {
background-color: rgba(50,50,50,0.3);
}
}
.cp-avatar,
.cp-app-contacts-right-col {
flex:1 1 auto;
}
.cp-app-contacts-remove-history {
.hover;
}
.cp-avatar {
margin: 10px;
}
.cp-app-contacts-more-history {
//display: none;
.hover;
&.cp-app-contacts-faded {
color: darken(@bg-color, 5%);
}
}
}
.cp-app-contacts-chat {
height: 100%;
display: flex;
flex-flow: column;
.cp-app-contacts-messages {
padding: 0 20px;
margin: 10px 0;
flex: 1;
overflow-x: auto;
.cp-app-contacts-message {
& > div {
padding: 0 10px;
}
.cp-app-contacts-content {
overflow: hidden;
word-wrap: break-word;
&> * {
margin: 0;
}
}
.cp-app-contacts-date {
display: none;
font-style: italic;
}
.cp-app-contacts-sender {
margin-top: 10px;
font-weight: bold;
background-color: rgba(0,0,0,0.1);
}
}
}
}
.cp-app-contacts-input {
background-color: lighten(@bg-color, 15%);
height: auto;
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 75px;
textarea {
margin: 5px 0;
padding: 0 10px;
border: none;
height: 50px;
flex: 1;
background-color: darken(@bg-color, 10%);
color: @color;
resize: none;
line-height: 50px;
overflow-y: auto;
.placeholder(#bbb);
&[disabled=true] {
.placeholder(#999);
}
&:placeholder-shown { line-height: 50px; }
}
button {
height: 50px;
border-radius: 0;
border: none;
background-color: darken(@bg-color, 15%);
&:hover {
background-color: darken(@bg-color, 20%);
}
}
}
}
} }

@ -41,22 +41,7 @@ define([
document.body.appendChild(toolbarElement); document.body.appendChild(toolbarElement);
var messaging = h('div#cp-app-contacts-messaging', [ var appElement = h('div#cp-app-contacts-container');
h('div.cp-app-contacts-info', [
h('h2', Messages.contacts_info1),
h('ul', [
h('li', Messages.contacts_info2),
h('li', Messages.contacts_info3),
])
])
]);
var friendList = h('div#cp-app-contacts-friendlist');
var appElement = h('div#cp-app-contacts-container', [
friendList,
messaging,
]);
document.body.appendChild(appElement); document.body.appendChild(appElement);
@ -73,7 +58,7 @@ define([
var messenger = Messenger.create(sFrameChan); var messenger = Messenger.create(sFrameChan);
MessengerUI.create(messenger, $(friendList), $(messaging), common); MessengerUI.create(messenger, $(appElement), common);
UI.removeLoadingScreen(); UI.removeLoadingScreen();

@ -3,59 +3,141 @@ 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-notifier.js',
'/common/hyperscript.js', '/common/hyperscript.js',
'/bower_components/marked/marked.min.js', '/bower_components/marked/marked.min.js',
'/common/media-tag.js', '/common/media-tag.js',
], function ($, Messages, Util, UI, Notifier, h, Marked, MediaTag) { ], function ($, Messages, Util, UI, h, Marked, MediaTag) {
'use strict'; 'use strict';
var debug = console.log;
debug = function () {};
var MessengerUI = {}; var MessengerUI = {};
var dataQuery = function (curvePublic) { var dataQuery = function (id) {
return '[data-key="' + curvePublic + '"]'; return '[data-key="' + id + '"]';
};
var userQuery = function (curve) {
return '[data-user="' + curve + '"]';
}; };
var initChannel = function (state, curvePublic, info) { var initChannel = function (state, info) {
console.log('initializing channel for [%s]', curvePublic); console.log('initializing channel for [%s]', info.id);
state.channels[curvePublic] = { var h, t;
messages: [], if (Array.isArray(info.messages) && info.messages.length) {
HEAD: info.lastKnownHash, h = info.messages[info.messages.length -1].sig;
TAIL: null, t = info.messages[0].sig;
}
state.channels[info.id] = {
messages: info.messages || [],
name: info.name,
isFriendChat: info.isFriendChat,
needMoreHistory: !info.isPadChat,
isPadChat: info.isPadChat,
curvePublic: info.curvePublic,
HEAD: h || info.lastKnownHash,
TAIL: t || null,
}; };
}; };
MessengerUI.create = function (messenger, $userlist, $messages, common) { MessengerUI.create = function (messenger, $container, common, toolbar) {
var origin = common.getMetadataMgr().getPrivateData().origin; var sframeChan = common.getSframeChannel();
var metadataMgr = common.getMetadataMgr();
var origin = metadataMgr.getPrivateData().origin;
var isApp = typeof(toolbar) !== "undefined";
$container.addClass('cp-app-contacts-initializing');
var messaging = h('div#cp-app-contacts-messaging', [
h('span.fa.fa-spinner.fa-pulse.fa-4x.fa-fw.cp-app-contacts-spinner'),
h('div.cp-app-contacts-info', [
h('h2', Messages.contacts_info1),
h('ul', [
h('li', Messages.contacts_info2),
h('li', Messages.contacts_info3),
h('li', Messages.contacts_info4),
])
])
]);
var friendList = h('div#cp-app-contacts-friendlist', [
h('span.fa.fa-spinner.fa-pulse.fa-4x.fa-fw.cp-app-contacts-spinner'),
h('div.cp-app-contacts-padchat.cp-app-contacts-category', [
h('div.cp-app-contacts-category-content')
]),
h('div.cp-app-contacts-friends.cp-app-contacts-category', [
h('div.cp-app-contacts-category-content'),
h('h2.cp-app-contacts-category-title', Messages.contacts_friends),
]),
h('div.cp-app-contacts-rooms.cp-app-contacts-category', [
h('div.cp-app-contacts-category-content'),
h('h2.cp-app-contacts-category-title', Messages.contacts_rooms),
]),
]);
var $userlist = $(friendList).appendTo($container);
var $messages = $(messaging).appendTo($container);
var state = window.state = { var state = window.state = {
active: '', active: '',
channels: {}
}; };
state.channels = {}; var contactsData = state.contactsData = {};
var displayNames = state.displayNames = {};
var avatars = state.avatars = {}; var avatars = state.avatars = {};
var setActive = function (curvePublic) { var setActive = function (id) {
state.active = curvePublic; state.active = id;
}; };
var isActive = function (curvePublic) { var isActive = function (id) {
return curvePublic === state.active; return id === state.active;
}; };
var find = {}; var find = {};
find.inList = function (curvePublic) { find.inList = function (id) {
return $userlist.find(dataQuery(curvePublic)); return $userlist.find(dataQuery(id));
};
var notifyToolbar = function () {
if (!toolbar || !toolbar['chat']) { return; }
if (toolbar['chat'].find('button').hasClass('cp-toolbar-button-active')) { return; }
toolbar['chat'].find('button').addClass('cp-toolbar-notification');
};
var notify = function (id) {
find.inList(id).addClass('cp-app-contacts-notify');
};
var unnotify = function (id) {
find.inList(id).removeClass('cp-app-contacts-notify');
}; };
var notify = function (curvePublic) { var onResize = function () {
find.inList(curvePublic).addClass('cp-app-contacts-notify'); // Don't update the width if we are in the contacts app
if (!isApp) { return; }
var w = $userlist[0].offsetWidth - $userlist[0].clientWidth;
$userlist.css('width', (68 + w)+'px');
}; };
var unnotify = function (curvePublic) { var reorderRooms = function () {
find.inList(curvePublic).removeClass('cp-app-contacts-notify'); var channels = Object.keys(state.channels).sort(function (a, b) {
var m1 = state.channels[a].messages.slice(-1)[0];
var m2 = state.channels[b].messages.slice(-1)[0];
if (!m2) { return !m1 ? 0 : 1; }
if (!m1) { return -1; }
return m1.time - m2.time;
});
channels.forEach(function (c, i) {
$userlist.find(dataQuery(c)).css('order', i);
});
// Make sure the width is correct even if there is a scrollbar
onResize();
}; };
var m = function (md) { $(window).on('resize', onResize);
var m = function (md, hour) {
var d = h('div.cp-app-contacts-content'); var d = h('div.cp-app-contacts-content');
try { try {
d.innerHTML = Marked(md || ''); d.innerHTML = Marked(md || '');
@ -73,6 +155,9 @@ define([
// activate media-tags // activate media-tags
$d.find('media-tag').each(function (i, e) { MediaTag(e); }); $d.find('media-tag').each(function (i, e) { MediaTag(e); });
var time = h('div.cp-app-contacts-time', hour);
$d.append(time);
} catch (e) { } catch (e) {
console.error(md); console.error(md);
console.error(e); console.error(e);
@ -82,25 +167,35 @@ define([
var markup = {}; var markup = {};
markup.message = function (msg) { markup.message = function (msg) {
if (msg.type !== 'MSG') { return; }
var curvePublic = msg.author; var curvePublic = msg.author;
var name = displayNames[msg.author]; var name = typeof msg.name !== "undefined" ? (msg.name || Messages.anonymous)
: contactsData[msg.author].displayName;
var d = msg.time ? new Date(msg.time) : undefined;
var day = d ? d.toLocaleDateString() : '';
var hour = d ? d.toLocaleTimeString() : '';
return h('div.cp-app-contacts-message', { return h('div.cp-app-contacts-message', {
title: msg.time? new Date(msg.time).toLocaleString(): '?', //title: time || '?',
'data-key': curvePublic, 'data-user': curvePublic,
'data-day': day
}, [ }, [
name? h('div.cp-app-contacts-sender', name): undefined, name? h('div.cp-app-contacts-sender', [
m(msg.text), h('span.cp-app-contacts-sender-name', name),
h('span.cp-app-contacts-sender-time', day)
]): undefined,
m(msg.text, hour),
]); ]);
}; };
var getChat = function (curvePublic) { var getChat = function (id) {
return $messages.find(dataQuery(curvePublic)); return $messages.find(dataQuery(id));
}; };
var normalizeLabels = function ($messagebox) { var normalizeLabels = function ($messagebox) {
$messagebox.find('div.cp-app-contacts-message').toArray().reduce(function (a, b) { $messagebox.find('div.cp-app-contacts-message').toArray().reduce(function (a, b) {
var $b = $(b); var $b = $(b);
if ($(a).data('key') === $b.data('key')) { if ($(a).data('user') === $b.data('user') &&
$(a).data('day') === $b.data('day')) {
$b.find('.cp-app-contacts-sender').hide(); $b.find('.cp-app-contacts-sender').hide();
return a; return a;
} }
@ -108,29 +203,31 @@ define([
}, []); }, []);
}; };
markup.chatbox = function (curvePublic, data) { markup.chatbox = function (id, data, curvePublic) {
var moreHistory = h('span.cp-app-contacts-more-history.fa.fa-history', { var moreHistory = h('span.cp-app-contacts-more-history.fa.fa-history', {
title: Messages.contacts_fetchHistory, title: Messages.contacts_fetchHistory,
}); });
var displayName = data.displayName;
var chan = state.channels[id];
var displayName = chan.name;
var fetching = false; var fetching = false;
var $moreHistory = $(moreHistory).click(function () { var $moreHistory = $(moreHistory).click(function () {
if (fetching) { return; } if (fetching) { return; }
// get oldest known message... // get oldest known message...
var channel = state.channels[curvePublic]; var channel = state.channels[id];
if (channel.exhausted) { if (channel.exhausted) {
return void $moreHistory.addClass('cp-app-contacts-faded'); return void $moreHistory.addClass('cp-app-contacts-faded');
} }
console.log('getting history'); debug('getting history');
var sig = channel.TAIL || channel.HEAD; var sig = channel.TAIL || channel.HEAD;
fetching = true; fetching = true;
var $messagebox = $(getChat(curvePublic)).find('.cp-app-contacts-messages'); var $messagebox = $(getChat(id)).find('.cp-app-contacts-messages');
messenger.getMoreHistory(curvePublic, sig, 10, function (e, history) { messenger.getMoreHistory(id, sig, 10, function (e, history) {
fetching = false; fetching = false;
if (e) { return void console.error(e); } if (e) { return void console.error(e); }
@ -139,13 +236,16 @@ define([
return; return;
} }
history.forEach(function (msg) { history.forEach(function (msg, i) {
if (channel.exhausted) { return; } if (channel.exhausted) { return; }
if (msg.sig) { if (msg.sig) {
if (i === 0 && history.length > 1 && msg.sig === channel.TAIL) {
// First message is usually the lastKnownHash, ignore it
return;
}
if (msg.sig === channel.TAIL) { if (msg.sig === channel.TAIL) {
console.error('No more messages to fetch'); console.error('No more messages to fetch');
channel.exhausted = true; channel.exhausted = true;
console.log(channel);
return void $moreHistory.addClass('cp-app-contacts-faded'); return void $moreHistory.addClass('cp-app-contacts-faded');
} else { } else {
channel.TAIL = msg.sig; channel.TAIL = msg.sig;
@ -156,9 +256,11 @@ define([
if (msg.type !== 'MSG') { return; } if (msg.type !== 'MSG') { return; }
// FIXME Schlameil the painter (performance does not scale well) // FIXME Schlameil the painter (performance does not scale well)
// XXX trust the server?
/*
if (channel.messages.some(function (old) { if (channel.messages.some(function (old) {
return msg.sig === old.sig; return msg.sig === old.sig;
})) { return; } })) { return; }*/
channel.messages.unshift(msg); channel.messages.unshift(msg);
var el_message = markup.message(msg); var el_message = markup.message(msg);
@ -176,22 +278,43 @@ define([
UI.confirm(Messages.contacts_confirmRemoveHistory, function (yes) { UI.confirm(Messages.contacts_confirmRemoveHistory, function (yes) {
if (!yes) { return; } if (!yes) { return; }
messenger.clearOwnedChannel(data.channel, function (e) { messenger.clearOwnedChannel(id, function (e) {
if (e) { if (e) {
console.error(e); console.error(e);
UI.alert(Messages.contacts_removeHistoryServerError); UI.alert(Messages.contacts_removeHistoryServerError);
return; return;
} }
// XXX clear the UI?
}); });
}); });
}); });
var avatar = h('div.cp-avatar'); var avatar = h('div.cp-avatar');
var header = h('div.cp-app-contacts-header', [
avatar, var headerContent = [avatar, moreHistory, data.isFriendCHat ? removeHistory : undefined];
moreHistory, if (isApp) {
removeHistory, headerContent = [
h('div.cp-app-contacts-header-title', Messages.contacts_padTitle),
moreHistory
];
}
var header = h('div.cp-app-contacts-header', headerContent);
var priv = metadataMgr.getPrivateData();
var closeTips = h('span.fa.fa-window-close.cp-app-contacts-tips-close');
var tips;
if (isApp && Util.find(priv.settings, ['general', 'hidetips', 'chat']) !== true) {
tips = h('div.cp-app-contacts-tips', [
closeTips,
Messages.contacts_warning
]); ]);
}
$(closeTips).click(function () {
$(tips).hide();
common.setAttribute(['general', 'hidetips', 'chat'], true);
});
var messages = h('div.cp-app-contacts-messages'); var messages = h('div.cp-app-contacts-messages');
var input = h('textarea', { var input = h('textarea', {
placeholder: Messages.contacts_typeHere placeholder: Messages.contacts_typeHere
@ -205,12 +328,13 @@ define([
]); ]);
var $avatar = $(avatar); var $avatar = $(avatar);
if (data.avatar && avatars[data.avatar]) { var friend = contactsData[curvePublic] || {};
$avatar.append(avatars[data.avatar]).append(rightCol); if (friend.avatar && avatars[friend.avatar]) {
$avatar.append(avatars[friend.avatar]).append(rightCol);
} else { } else {
common.displayAvatar($avatar, data.avatar, data.displayName, function ($img) { common.displayAvatar($avatar, friend.avatar, displayName, function ($img) {
if (data.avatar && $img) { if (friend.avatar && $img) {
avatars[data.avatar] = $img[0].outerHTML; avatars[friend.avatar] = $img[0].outerHTML;
} }
$(rightCol).insertAfter($avatar); $(rightCol).insertAfter($avatar);
}); });
@ -221,14 +345,14 @@ define([
if (typeof(content) !== 'string' || !content.trim()) { return; } if (typeof(content) !== 'string' || !content.trim()) { return; }
if (sending) { return false; } if (sending) { return false; }
sending = true; sending = true;
messenger.sendMessage(curvePublic, content, function (e) { messenger.sendMessage(id, content, function (e) {
if (e) { if (e) {
// failed to send // failed to send
return void console.error('failed to send'); return void console.error('failed to send');
} }
input.value = ''; input.value = '';
sending = false; sending = false;
console.log('sent successfully'); debug('sent successfully');
var $messagebox = $(messages); var $messagebox = $(messages);
var height = $messagebox[0].scrollHeight; var height = $messagebox[0].scrollHeight;
@ -268,9 +392,11 @@ define([
$(sendButton).click(function () { send(input.value); }); $(sendButton).click(function () { send(input.value); });
return h('div.cp-app-contacts-chat', { return h('div.cp-app-contacts-chat', {
'data-key': curvePublic, 'data-key': id,
'data-user': data.isFriendChat && curvePublic
}, [ }, [
header, header,
tips,
messages, messages,
h('div.cp-app-contacts-input', [ h('div.cp-app-contacts-input', [
input, input,
@ -283,16 +409,20 @@ define([
$messages.find('.cp-app-contacts-info').hide(); $messages.find('.cp-app-contacts-info').hide();
}; };
var updateStatus = function (curvePublic) { var showInfo = function () {
var $status = find.inList(curvePublic).find('.cp-app-contacts-status'); $messages.find('.cp-app-contacts-info').show();
// FIXME this stopped working :( };
messenger.getStatus(curvePublic, function (e, online) {
var updateStatus = function (id) {
if (!state.channels[id]) { return; }
var $status = find.inList(id).find('.cp-app-contacts-status');
messenger.getStatus(id, function (e, online) {
// if error maybe you shouldn't display this friend... // if error maybe you shouldn't display this friend...
if (e) { if (e) {
find.inList(curvePublic).hide(); find.inList(id).hide();
getChat(curvePublic).hide(); getChat(id).hide();
return void console.error(curvePublic, e); return void console.error(id, e);
} }
if (online) { if (online) {
return void $status return void $status
@ -302,72 +432,81 @@ define([
}); });
}; };
var display = function (curvePublic) { var display = function (chanId) {
var channel = state.channels[curvePublic]; var channel = state.channels[chanId];
var lastMsg = channel.messages.slice(-1)[0]; var lastMsg = channel.messages.slice(-1)[0];
if (lastMsg) { if (lastMsg) {
channel.HEAD = lastMsg.sig; channel.HEAD = lastMsg.sig;
messenger.setChannelHead(curvePublic, channel.HEAD, function (e) { messenger.setChannelHead(chanId, channel.HEAD, function (e) {
if (e) { console.error(e); } if (e) { console.error(e); }
}); });
} }
setActive(curvePublic); setActive(chanId);
unnotify(curvePublic); unnotify(chanId);
var $chat = getChat(curvePublic); var $chat = getChat(chanId);
hideInfo(); hideInfo();
$messages.find('div.cp-app-contacts-chat[data-key]').hide(); $messages.find('div.cp-app-contacts-chat[data-key]').hide();
if ($chat.length) { if ($chat.length) {
var $chat_messages = $chat.find('div.cp-app-contacts-message'); var $chat_messages = $chat.find('div.cp-app-contacts-message');
if (!$chat_messages.length) { if ($chat_messages.length < 10) { //|| channel.needMoreHistory) { XXX
delete channel.needMoreHistory;
var $more = $chat.find('.cp-app-contacts-more-history'); var $more = $chat.find('.cp-app-contacts-more-history');
$more.click(); $more.click();
} }
return void $chat.show(); $chat.show();
return;
} else {
console.error("Chat is missing... Please reload the page and try again.");
} }
messenger.getFriendInfo(curvePublic, function (e, info) {
if (e) { return void console.error(e); } // FIXME
var chatbox = markup.chatbox(curvePublic, info);
$messages.append(chatbox);
});
}; };
var removeFriend = function (curvePublic) { var removeFriend = function (curvePublic) {
messenger.removeFriend(curvePublic, function (e /*, removed */) { messenger.removeFriend(curvePublic, function (e /*, removed */) {
if (e) { return void console.error(e); } if (e) { return void console.error(e); }
find.inList(curvePublic).remove();
//console.log(removed);
}); });
}; };
markup.friend = function (data) { markup.room = function (id, room, userlist) {
var curvePublic = data.curvePublic; var roomEl = h('div.cp-app-contacts-friend.cp-avatar', {
var friend = h('div.cp-app-contacts-friend.cp-avatar', { 'data-key': id,
'data-key': curvePublic, 'data-user': room.isFriendChat ? userlist[0].curvePublic : '',
title: room.name
}); });
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
}); });
var status = h('span.cp-app-contacts-status'); var leaveRoom = h('span.cp-app-contacts-remove.fa.fa-sign-out', {
title: Messages.contacts_leaveRoom
});
var status = h('span.cp-app-contacts-status', {
title: Messages.contacts_online
});
var rightCol = h('span.cp-app-contacts-right-col', [ var rightCol = h('span.cp-app-contacts-right-col', [
h('span.cp-app-contacts-name', [data.displayName]), h('span.cp-app-contacts-name', [room.name]),
remove, room.isFriendChat ? remove :
room.isPadChat ? undefined : leaveRoom,
]); ]);
var $friend = $(friend) var friendData = room.isFriendChat ? userlist[0] : {};
.click(function () {
display(curvePublic); var $room = $(roomEl).click(function () {
}) display(id);
.dblclick(function () { }).dblclick(function () {
if (data.profile) { window.open(origin + '/profile/#' + data.profile); } if (friendData.profile) { window.open(origin + '/profile/#' + friendData.profile); }
}); });
$(remove).click(function (e) { $(remove).click(function (e) {
e.stopPropagation(); e.stopPropagation();
var channel = state.channels[id];
if (!channel.isFriendChat) { return; }
var curvePublic = channel.curvePublic;
var friend = contactsData[curvePublic] || friendData;
UI.confirm(Messages._getKey('contacts_confirmRemove', [ UI.confirm(Messages._getKey('contacts_confirmRemove', [
Util.fixHTML(data.displayName) Util.fixHTML(friend.name)
]), function (yes) { ]), function (yes) {
if (!yes) { return; } if (!yes) { return; }
removeFriend(curvePublic, function (e) { removeFriend(curvePublic, function (e) {
@ -379,41 +518,42 @@ define([
}, undefined, true); }, undefined, true);
}); });
if (data.avatar && avatars[data.avatar]) { if (friendData.avatar && avatars[friendData.avatar]) {
$friend.append(avatars[data.avatar]); $room.append(avatars[friendData.avatar]);
$friend.append(rightCol); $room.append(rightCol);
} else { } else {
common.displayAvatar($friend, data.avatar, data.displayName, function ($img) { common.displayAvatar($room, friendData.avatar, room.name, function ($img) {
if (data.avatar && $img) { if (friendData.avatar && $img) {
avatars[data.avatar] = $img[0].outerHTML; avatars[friendData.avatar] = $img[0].outerHTML;
} }
$friend.append(rightCol); $room.append(rightCol);
}); });
} }
$friend.append(status); $room.append(status);
return $friend; return $room;
}; };
var isBottomedOut = function ($elem) { var isBottomedOut = function ($elem) {
return ($elem[0].scrollHeight - $elem.scrollTop() === $elem.outerHeight()); return ($elem[0].scrollHeight - $elem.scrollTop() === $elem.outerHeight());
}; };
var initializing = true;
messenger.on('message', function (message) { messenger.on('message', function (message) {
if (!initializing) { Notifier.notify(); } var chanId = message.channel;
var curvePublic = message.curve; var channel = state.channels[chanId];
if (!channel) { return; }
var name = displayNames[curvePublic]; var chat = getChat(chanId);
var chat = getChat(curvePublic, name);
console.log(message); debug(message);
var el_message = markup.message(message); var el_message = markup.message(message);
state.channels[curvePublic].messages.push(message); common.notify();
notifyToolbar();
var $chat = $(chat); channel.messages.push(message);
var $chat = $(chat);
if (!$chat.length) { if (!$chat.length) {
console.error("Got a message but the chat isn't open"); console.error("Got a message but the chat isn't open");
} }
@ -427,111 +567,266 @@ define([
$messagebox.scrollTop($messagebox.outerHeight()); $messagebox.scrollTop($messagebox.outerHeight());
} }
normalizeLabels($messagebox); normalizeLabels($messagebox);
reorderRooms();
var channel = state.channels[curvePublic]; if (isActive(chanId)) {
if (!channel) {
console.error('expected channel [%s] to be open', curvePublic);
return;
}
if (isActive(curvePublic)) {
channel.HEAD = message.sig; channel.HEAD = message.sig;
messenger.setChannelHead(curvePublic, message.sig, function (e) { messenger.setChannelHead(chanId, message.sig, function (e) {
if (e) { return void console.error(e); } if (e) { return void console.error(e); }
}); });
return; return;
} }
var lastMsg = channel.messages.slice(-1)[0]; var lastMsg = channel.messages.slice(-1)[0];
if (lastMsg.sig !== channel.HEAD) { if (lastMsg.sig !== channel.HEAD) {
return void notify(curvePublic); return void notify(chanId);
} }
unnotify(curvePublic); unnotify(chanId);
}); });
messenger.on('join', function (curvePublic, channel) { messenger.on('join', function (data, channel) {
channel = channel; if (data.curvePublic) {
updateStatus(curvePublic); contactsData[data.curvePublic] = data;
}
updateStatus(channel);
// TODO room refresh online userlist
}); });
messenger.on('leave', function (curvePublic, channel) { messenger.on('leave', function (data, channel) {
channel = channel; if (contactsData[data.curvePublic]) {
updateStatus(curvePublic); delete contactsData[data.curvePublic];
}
updateStatus(channel);
// TODO room refresh online userlist
}); });
// change in your friend list // change in your friend list
messenger.on('update', function (info, curvePublic) { messenger.on('update', function (info, types, channel) {
var name = displayNames[curvePublic] = info.displayName; if (!info || !info.curvePublic) { return; }
// Make sure we don't store useless data (friends data in pad chat or the other way)
if (channel && !state.channels[channel]) { return; }
var curvePublic = info.curvePublic;
contactsData[curvePublic] = info;
if (types.indexOf('displayName') !== -1) {
var name = info.displayName;
// update label in friend list // update label in friend list
find.inList(curvePublic).find('.cp-app-contacts-name').text(name); $userlist.find(userQuery(curvePublic)).find('.cp-app-contacts-name').text(name);
$userlist.find(userQuery(curvePublic)).attr('title', name);
// update title bar and messages // update title bar and messages
$messages.find(dataQuery(curvePublic) + ' .cp-app-contacts-header ' + $messages.find(userQuery(curvePublic) + ' .cp-app-contacts-header ' +
'.cp-app-contacts-name, div.cp-app-contacts-message'+ '.cp-app-contacts-name, div.cp-app-contacts-message'+
dataQuery(curvePublic) + ' div.cp-app-contacts-sender').text(name).text(name); userQuery(curvePublic) + ' div.cp-app-contacts-sender').text(name);
// TODO room
// Update name in room userlist
}
if (types.indexOf('profile') !== -1) {
// update dblclick event in friend list
$userlist.find(userQuery(curvePublic)).off('dblclick').dblclick(function () {
if (info.profile) { window.open(origin + '/profile/#' + info.profile); }
}); });
}
var connectToFriend = function (curvePublic, cb) { if (types.indexOf('avatar') !== -1) {
messenger.getFriendInfo(curvePublic, function (e, info) { var $mAvatar = $messages
if (e) { return void console.error(e); } .find(userQuery(curvePublic) +' .cp-app-contacts-header .cp-avatar');
var name = displayNames[curvePublic] = info.displayName; var $lAvatar = $userlist.find(userQuery(curvePublic));
initChannel(state, curvePublic, info); $lAvatar.find('.cp-avatar-default, media-tag').remove();
var chatbox = markup.chatbox(curvePublic, info); var $div = $('<div>');
common.displayAvatar($div, info.avatar, info.displayName, function ($img) {
if (info.avatar && $img) {
avatars[info.avatar] = $img[0].outerHTML;
}
$mAvatar.html($div.html());
$lAvatar.find('.cp-app-contacts-right-col').before($div.html());
});
}
});
var execCommand = function (cmd, data, cb) {
sframeChan.query('Q_CHAT_COMMAND', {cmd: cmd, data: data}, function (err, obj) {
if (err || (obj && obj.error)) { return void cb(err || (obj && obj.error)); }
cb(void 0, obj);
});
};
var initializeRoom = function (room) {
var id = room.id;
initChannel(state, room);
execCommand('GET_USERLIST', {id: id}, function (e, list) {
if (e || list.error) { return void console.error(e || list.error); }
if (!room.isPadChat && (!Array.isArray(list) || !list.length)) {
return void console.error("Empty room!");
}
debug('userlist: ' + JSON.stringify(list), id);
var friend = {};
if (room.isFriendChat) {
// This is a friend, the userlist is only one user.
friend = list[0];
contactsData[friend.curvePublic] = friend;
}
var chatbox = markup.chatbox(id, room, friend.curvePublic);
$(chatbox).hide(); $(chatbox).hide();
$messages.append(chatbox); $messages.append(chatbox);
var friend = markup.friend(info, name); var $messagebox = $(chatbox).find('.cp-app-contacts-messages');
$userlist.append(friend); room.messages.forEach(function (msg) {
messenger.openFriendChannel(curvePublic, function (e) { var el_message = markup.message(msg);
if (e) { return void console.error(e); } $messagebox.append(el_message);
cb();
updateStatus(curvePublic);
// don't add friends that are already in your userlist
//if (friendExistsInUserList(k)) { return; }
}); });
normalizeLabels($messagebox);
var roomEl = markup.room(id, room, list);
var $parentEl;
if (room.isFriendChat) {
$parentEl = $userlist.find('.cp-app-contacts-friends');
} else if (room.isPadChat) {
$parentEl = $userlist.find('.cp-app-contacts-padchat');
} else {
$parentEl = $userlist.find('.cp-app-contacts-rooms');
}
$parentEl.find('.cp-app-contacts-category-content').append(roomEl);
reorderRooms();
updateStatus(id);
if (isApp && room.isPadChat) {
$container.removeClass('cp-app-contacts-initializing');
display(room.id);
}
}); });
}; };
messenger.on('friend', function (curvePublic) { messenger.on('friend', function (curvePublic) {
console.log('new friend: ', curvePublic); if (isApp) { return; }
//console.error("TODO redraw user list"); debug('new friend: ', curvePublic);
//console.error("TODO connect to new friend"); execCommand('GET_ROOMS', {curvePublic: curvePublic}, function (err, rooms) {
// FIXME this doesn't work right now because the friend hasn't been fully added? if (err) { return void console.error(err); }
connectToFriend(curvePublic, function () { debug('rooms: ' + JSON.stringify(rooms));
//console.error('connected'); rooms.forEach(initializeRoom);
}); });
});
messenger.on('unfriend', function (curvePublic, removedByMe) {
if (isApp) { return; }
var channel = state.channels[state.active];
$userlist.find(userQuery(curvePublic)).remove();
$messages.find(userQuery(curvePublic)).remove();
if (channel && channel.curvePublic === curvePublic) {
showInfo();
}
if (!removedByMe) {
// TODO UI.alert if this is triggered by the other guy
}
}); });
messenger.on('unfriend', function (curvePublic) { common.getMetadataMgr().onTitleChange(function () {
console.log('unfriend', curvePublic); var padChat = common.getPadChat();
find.inList(curvePublic).remove(); var md = common.getMetadataMgr().getMetadata();
console.error('TODO remove chatbox'); var name = md.title || md.defaultTitle;
console.error('TODO show something if that chatbox was active'); $userlist.find(dataQuery(padChat)).find('.cp-app-contacts-name').text(name);
}); $userlist.find(dataQuery(padChat)).attr('title', name);
$messages.find(dataQuery(padChat) + ' .cp-app-contacts-header .cp-app-contacts-name')
.text(name);
var $mAvatar = $messages.find(dataQuery(padChat) +' .cp-app-contacts-header .cp-avatar');
var $lAvatar = $userlist.find(dataQuery(padChat));
$lAvatar.find('.cp-avatar-default, media-tag').remove();
common.getMetadataMgr().onChange(function () { var $div = $('<div>');
//messenger.checkNewFriends(); common.displayAvatar($div, null, name, function () {
messenger.updateMyData(); $mAvatar.html($div.html());
$lAvatar.find('.cp-app-contacts-right-col').before($div.html());
}); });
});
// TODO room
// messenger.on('joinroom', function (chanid))
// messenger.on('leaveroom', function (chanid))
// FIXME dirty hack
messenger.getMyInfo(function (e, info) { messenger.getMyInfo(function (e, info) {
displayNames[info.curvePublic] = info.displayName; contactsData[info.curvePublic] = info;
}); });
messenger.getFriendList(function (e, keys) { var ready = false;
var count = keys.length + 1; var onMessengerReady = function () {
var ready = function () { if (isApp) { return; }
count--; if (ready) { return; }
if (count === 0) { ready = true;
initializing = false;
UI.removeLoadingScreen(); execCommand('GET_ROOMS', null, function (err, rooms) {
if (err) { return void console.error(err); }
debug('rooms: ' + JSON.stringify(rooms));
rooms.forEach(initializeRoom);
});
$container.removeClass('cp-app-contacts-initializing');
};
var onPadChatReady = function (data) {
var padChat = common.getPadChat();
if (data !== padChat) { return; }
if (state.channels[data]) { return; }
execCommand('GET_ROOMS', {padChat: data}, function (err, rooms) {
if (err) { return void console.error(err); }
if (!Array.isArray(rooms) || rooms.length !== 1) {
return void console.error('Invalid pad chat');
} }
var room = rooms[0];
var md = common.getMetadataMgr().getMetadata();
var name = md.title || md.defaultTitle;
room.name = name;
rooms.forEach(initializeRoom);
});
}; };
ready();
keys.forEach(function (curvePublic) { var onDisconnect = function () {
connectToFriend(curvePublic, ready); debug('disconnected');
$messages.find('.cp-app-contacts-input textarea').prop('disabled', true);
};
var onReconnect = function () {
debug('reconnected');
$messages.find('.cp-app-contacts-input textarea').prop('disabled', false);
};
// Initialize chat when outer is ready (all channels loaded)
// TODO: try again in outer if fail to load a channel
if (!isApp) {
execCommand('INIT_FRIENDS', null, function () {});
execCommand('IS_READY', null, function (err, yes) {
if (yes) { onMessengerReady(); }
}); });
}
sframeChan.on('EV_CHAT_EVENT', function (obj) {
if (obj.ev === 'READY') {
onMessengerReady();
return;
}
if (obj.ev === 'PADCHAT_READY') {
onPadChatReady(obj.data);
return;
}
if (obj.ev === 'DISCONNECT') {
onDisconnect();
return;
}
if (obj.ev === 'RECONNECT') {
onReconnect();
return;
}
}); });
}; };

@ -653,8 +653,10 @@
#cp-app-drive-new-ghost-dialog.cp-modal-container { #cp-app-drive-new-ghost-dialog.cp-modal-container {
.drive_fileIcon; .drive_fileIcon;
border: 1px solid @colortheme_modal-fg;
li:not(.cp-app-drive-element-selected):hover { li:not(.cp-app-drive-element-selected):hover {
border: 1px solid white; background: @colortheme_modal-fg;
color: @colortheme_modal-bg;
} }
.cp-modal { .cp-modal {
display: flex; display: flex;
@ -663,14 +665,16 @@
cursor: pointer; cursor: pointer;
} }
&> p { &> p {
margin: 50px; display: flex;
align-items: center;
justify-content: center;
} }
&> div { &> div {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
align-content: center; align-content: center;
overflow-y: auto; flex: 1;
.cp-app-drive-new-upload { .cp-app-drive-new-upload {
break-after: always; break-after: always;
page-break-after: always; page-break-after: always;
@ -682,23 +686,27 @@
white-space: nowrap; white-space: nowrap;
} }
@media screen and (max-height: @browser_media-not-big) { @media screen and (max-width: @browser_media-medium-screen),
screen and (max-height: @browser_media-medium-screen) {
.cp-modal { .cp-modal {
& > p { & > p {
display: none; display: none;
} }
& > div { & > div {
align-content: unset;
li { li {
height: 40px; height: 40px;
width: 90%; width: 90%;
display: flex; display: flex;
align-items: center; align-items: center;
align-content: unset;
.fa { .fa {
font-size: 32px; font-size: 32px;
min-width: 50px;
} }
.cp-app-drive-new-name { .cp-app-drive-new-name {
height: auto; height: auto;
overflow: hidden;
text-overflow: ellipsis;
} }
} }
} }

@ -1207,15 +1207,23 @@ define([
if (paths) { if (paths) {
paths.forEach(function (p) { pathsList.push(p.path); }); paths.forEach(function (p) { pathsList.push(p.path); });
} }
var hasOwned = pathsList.some(function (p) {
var el = manager.find(p);
var data = manager.isSharedFolder(el) ? manager.getSharedFolderData(el)
: manager.getFileData(el);
return data.owners && data.owners.indexOf(edPublic) !== -1;
});
var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [pathsList.length]); var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [pathsList.length]);
if (pathsList.length === 1) { if (pathsList.length === 1) {
msg = Messages.fm_removePermanentlyDialog; msg = hasOwned ? Messages.fm_deleteOwnedPad : Messages.fm_removePermanentlyDialog;
} else if (hasOwned) {
msg = msg + '<br><em>' + Messages.fm_removePermanentlyNote + '</em>';
} }
UI.confirm(msg, function(res) { UI.confirm(msg, function(res) {
$(window).focus(); $(window).focus();
if (!res) { return; } if (!res) { return; }
manager.delete(pathsList, refresh); manager.delete(pathsList, refresh);
}); }, null, true);
}; };
// Drag & drop: // Drag & drop:
// The data transferred is a stringified JSON containing the path of the dragged element // The data transferred is a stringified JSON containing the path of the dragged element
@ -1290,6 +1298,7 @@ define([
if (!oldPaths) { return; } if (!oldPaths) { return; }
// A moved element should be removed from its previous location // A moved element should be removed from its previous location
var movedPaths = []; var movedPaths = [];
var sharedF = false; var sharedF = false;
oldPaths.forEach(function (p) { oldPaths.forEach(function (p) {
movedPaths.push(p.path); movedPaths.push(p.path);
@ -1301,9 +1310,25 @@ define([
var newPath = findDropPath(ev.target); var newPath = findDropPath(ev.target);
if (!newPath) { return; } if (!newPath) { return; }
if (sharedF && manager.isPathIn(newPath, [TRASH])) { if (sharedF && manager.isPathIn(newPath, [TRASH])) {
deletePaths(null, movedPaths); return void deletePaths(null, movedPaths);
return;
} }
if (manager.isPathIn(newPath, [TRASH])) {
// Filter the selection to remove shared folders.
// Shared folders can't be moved to the trash!
var filteredPaths = movedPaths.filter(function (p) {
var el = manager.find(p);
return !manager.isSharedFolder(el);
});
if (!filteredPaths.length) {
// We only have shared folder, delete them
return void deletePaths(null, movedPaths);
}
movedPaths = filteredPaths;
}
if (movedPaths && movedPaths.length) { if (movedPaths && movedPaths.length) {
moveElements(movedPaths, newPath, null, refresh); moveElements(movedPaths, newPath, null, refresh);
} }
@ -1805,15 +1830,18 @@ define([
.click(function () { .click(function () {
var $input = $('<input>', { var $input = $('<input>', {
'type': 'file', 'type': 'file',
'style': 'display: none;' 'style': 'display: none;',
'multiple': 'multiple'
}).on('change', function (e) { }).on('change', function (e) {
var file = e.target.files[0]; var files = Util.slice(e.target.files);
files.forEach(function (file) {
var ev = { var ev = {
target: $content[0], target: $content[0],
path: findDropPath($content[0]) path: findDropPath($content[0])
}; };
APP.FM.handleFile(file, ev); APP.FM.handleFile(file, ev);
}); });
});
$input.click(); $input.click();
}); });
} }
@ -1889,9 +1917,11 @@ define([
var createShareButton = function (id, $container) { var createShareButton = function (id, $container) {
var $shareBlock = $('<button>', { var $shareBlock = $('<button>', {
'class': 'fa fa-shhare-alt cp-toolbar-share-button', 'class': 'cp-toolbar-share-button',
title: Messages.shareButton title: Messages.shareButton
}); });
$sharedIcon.clone().appendTo($shareBlock);
$('<span>').text(Messages.shareButton).appendTo($shareBlock);
var data = manager.getSharedFolderData(id); var data = manager.getSharedFolderData(id);
var parsed = Hash.parsePadUrl(data.href); var parsed = Hash.parsePadUrl(data.href);
if (!parsed || !parsed.hash) { return void console.error("Invalid href: "+data.href); } if (!parsed || !parsed.hash) { return void console.error("Invalid href: "+data.href); }
@ -2574,7 +2604,12 @@ define([
createNewButton(isInRoot, $toolbar.find('.cp-app-drive-toolbar-leftside')); createNewButton(isInRoot, $toolbar.find('.cp-app-drive-toolbar-leftside'));
var sfId = manager.isInSharedFolder(currentPath); var sfId = manager.isInSharedFolder(currentPath);
if (sfId) { if (sfId) {
var sfData = manager.getSharedFolderData(sfId);
var parsed = Hash.parsePadUrl(sfData.href);
sframeChan.event('EV_DRIVE_SET_HASH', parsed.hash || '');
createShareButton(sfId, $toolbar.find('.cp-app-drive-toolbar-leftside')); createShareButton(sfId, $toolbar.find('.cp-app-drive-toolbar-leftside'));
} else {
sframeChan.event('EV_DRIVE_SET_HASH', '');
} }
createTitle($toolbar.find('.cp-app-drive-path'), path); createTitle($toolbar.find('.cp-app-drive-path'), path);
@ -2721,7 +2756,7 @@ define([
} }
var dataPath = isSharedFolder ? path.slice(0, -1) : path; var dataPath = isSharedFolder ? path.slice(0, -1) : path;
$elementRow.data('path', dataPath); $elementRow.data('path', dataPath);
addDragAndDropHandlers($elementRow, path, true, droppable); addDragAndDropHandlers($elementRow, dataPath, true, droppable);
if (active) { if (active) {
$elementRow.addClass('cp-app-drive-element-active cp-leftside-active'); $elementRow.addClass('cp-app-drive-element-active cp-leftside-active');
} }
@ -3221,16 +3256,23 @@ define([
paths.push($(elmt).data('path')); paths.push($(elmt).data('path'));
}); });
if (!paths.length) { return; } if (!paths.length) { return; }
// Remove shared folders from the selection (they can't be moved to the trash)
// unless the selection is only shared folders
var paths2 = paths.filter(function (p) {
var el = manager.find(p);
return !manager.isSharedFolder(el);
});
// If we are in the trash or anon pad or if we are holding the "shift" key, // If we are in the trash or anon pad or if we are holding the "shift" key,
// delete permanently // delete permanently
// Or if we are in a shared folder // Or if we are in a shared folder
// Or if the selection is only shared folders
if (!APP.loggedIn || isTrash || manager.isInSharedFolder(currentPath) if (!APP.loggedIn || isTrash || manager.isInSharedFolder(currentPath)
|| e.shiftKey) { || e.shiftKey || !paths2.length) {
deletePaths(null, paths); deletePaths(null, paths);
return; return;
} }
// else move to trash // else move to trash
moveElements(paths, [TRASH], false, refresh); moveElements(paths2, [TRASH], false, refresh);
return; return;
} }
}); });

@ -41,7 +41,7 @@ define([
var secret = Utils.Hash.getSecrets('drive', hash); var secret = Utils.Hash.getSecrets('drive', hash);
if (hash) { if (hash) {
// Add a shared folder! // Add a shared folder!
// XXX password? // TODO password?
Cryptpad.addSharedFolder(secret, function (id) { Cryptpad.addSharedFolder(secret, function (id) {
window.CryptPad_newSharedFolder = id; window.CryptPad_newSharedFolder = id;
// Update the hash in the address bar // Update the hash in the address bar
@ -87,6 +87,14 @@ define([
cb(obj); cb(obj);
}); });
}); });
sframeChan.on('EV_DRIVE_SET_HASH', function (hash) {
// Update the hash in the address bar
var ohc = window.onhashchange;
window.onhashchange = function () {};
window.location.hash = hash || '';
window.onhashchange = ohc;
ohc({reset:true});
});
Cryptpad.onNetworkDisconnect.reg(function () { Cryptpad.onNetworkDisconnect.reg(function () {
sframeChan.event('EV_NETWORK_DISCONNECT'); sframeChan.event('EV_NETWORK_DISCONNECT');
}); });

@ -67,7 +67,6 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
#kanban-edit { #kanban-edit {
color: black;
font-weight: bold; font-weight: bold;
} }
} }
@ -77,6 +76,7 @@
width: 100%; width: 100%;
background: transparent; background: transparent;
border: 1px solid rgba(0,0,0,0.3); border: 1px solid rgba(0,0,0,0.3);
color: inherit;
} }
@button-size: 50px; @button-size: 50px;
@ -117,39 +117,39 @@
} }
.kanban-header-yellow { .kanban-header-yellow {
background: #FC3; background: #FC3 !important;
} }
.kanban-header-orange { .kanban-header-orange {
background: #F91; background: #F91 !important;
} }
.kanban-header-blue { .kanban-header-blue {
background: #0AC; background: #0AC !important;
} }
.kanban-header-red { .kanban-header-red {
background: #E43; background: #E43 !important;
} }
.kanban-header-green { .kanban-header-green {
background: #8C4; background: #8C4 !important;
} }
.kanban-header-purple { .kanban-header-purple {
background: #c851ff; background: #c851ff !important;
} }
.kanban-header-cyan { .kanban-header-cyan {
background: #00ffff; background: #00ffff !important;
} }
.kanban-header-lightgreen { .kanban-header-lightgreen {
background: #c3ff5b; background: #c3ff5b !important;
} }
.kanban-header-lightblue { .kanban-header-lightblue {
background: #adeeff; background: #adeeff !important;
} }
@media (max-width: @browser_media-medium-screen) { @media (max-width: @browser_media-medium-screen) {

@ -10,6 +10,7 @@ define([
'/common/modes.js', '/common/modes.js',
'/customize/messages.js', '/customize/messages.js',
'/kanban/jkanban.js', '/kanban/jkanban.js',
'/kanban/jscolor.js',
'css!/kanban/jkanban.css', 'css!/kanban/jkanban.css',
'less!/kanban/app-kanban.less' 'less!/kanban/app-kanban.less'
@ -107,7 +108,7 @@ define([
var kanban = new window.jKanban({ var kanban = new window.jKanban({
element: '#cp-app-kanban-content', element: '#cp-app-kanban-content',
gutter: '15px', gutter: '5px',
widthBoard: '300px', widthBoard: '300px',
buttonContent: '❌', buttonContent: '❌',
colors: COLORS, colors: COLORS,
@ -138,7 +139,7 @@ define([
// Remove the input // Remove the input
$(el).text(name); $(el).text(name);
// Save the value for the correct board // Save the value for the correct board
var board = $(el.parentNode.parentNode).attr("data-id"); var board = $(el.parentNode.parentNode.parentNode).attr("data-id");
var pos = kanban.findElementPosition(el); var pos = kanban.findElementPosition(el);
kanban.getBoardJSON(board).item[pos].title = name; kanban.getBoardJSON(board).item[pos].title = name;
kanban.onChange(); kanban.onChange();
@ -206,24 +207,53 @@ define([
} }
}); });
}, },
colorClick: function (el) { colorClick: function (el, type) {
if (framework.isReadOnly() || framework.isLocked()) { return; } if (framework.isReadOnly() || framework.isLocked()) { return; }
verbose("in color click"); verbose("on color click");
var board = $(el.parentNode).attr("data-id"); var boardJSON;
var boardJSON = kanban.getBoardJSON(board); var board;
var currentColor = boardJSON.color; if (type === "board") {
verbose("board color click");
board = $(el.parentNode).attr("data-id");
boardJSON = kanban.getBoardJSON(board);
} else {
verbose("item color click");
board = $(el.parentNode.parentNode).attr("data-id");
var pos = kanban.findElementPosition(el);
boardJSON = kanban.getBoardJSON(board).item[pos];
}
var onchange = function (colorL) {
var elL = el;
var typeL = type;
var boardJSONL;
var boardL;
if (typeL === "board") {
verbose("board color change");
boardL = $(elL.parentNode).attr("data-id");
boardJSONL = kanban.getBoardJSON(boardL);
} else {
verbose("item color change");
boardL = $(elL.parentNode.parentNode).attr("data-id");
var pos = kanban.findElementPosition(elL);
boardJSONL = kanban.getBoardJSON(boardL).item[pos];
}
var currentColor = boardJSONL.color;
verbose("Current color " + currentColor); verbose("Current color " + currentColor);
var index = kanban.options.colors.findIndex(function (element) { if (currentColor !== colorL.toString()) {
return (element === currentColor); $(elL).removeClass("kanban-header-" + currentColor);
}) + 1; boardJSONL.color = colorL.toString();
verbose("Next index " + index);
if (index >= kanban.options.colors.length) { index = 0; }
var nextColor = kanban.options.colors[index];
verbose("Next color " + nextColor);
boardJSON.color = nextColor;
$(el).removeClass("kanban-header-" + currentColor);
$(el).addClass("kanban-header-" + nextColor);
kanban.onChange(); kanban.onChange();
}
};
var jscolorL;
el._jscLinkedInstance = undefined;
jscolorL = new window.jscolor(el,{onFineChange: onchange, valueElement:undefined});
jscolorL.show();
var currentColor = boardJSON.color;
if (currentColor === undefined) {
currentColor = '';
}
jscolorL.fromString(currentColor);
}, },
buttonClick: function (el, boardId, e) { buttonClick: function (el, boardId, e) {
e.stopPropagation(); e.stopPropagation();

@ -52,6 +52,7 @@
widthBoard: '250px', widthBoard: '250px',
responsive: '700', responsive: '700',
colors: ["yellow", "green", "blue", "red", "orange"], colors: ["yellow", "green", "blue", "red", "orange"],
responsivePercentage: false,
boards: [], boards: [],
dragBoards: true, dragBoards: true,
addItemButton: false, addItemButton: false,
@ -67,7 +68,7 @@
click: function (el) {}, click: function (el) {},
boardTitleclick: function (el, boardId) {}, boardTitleclick: function (el, boardId) {},
buttonClick: function (el, boardId) {}, buttonClick: function (el, boardId) {},
colorClick: function (el, boardId) {}, colorClick: function (el, type) {},
addItemClick: function (el, boardId) {}, addItemClick: function (el, boardId) {},
onChange: function () {} onChange: function () {}
}; };
@ -85,12 +86,12 @@
//Init Drag Board //Init Drag Board
self.drakeBoard = self.dragula([self.container], { self.drakeBoard = self.dragula([self.container], {
moves: function (el, source, handle, sibling) { moves: function (el, source, handle, sibling) {
if (self.options.readOnly) { return false; } if (self.options.readOnly) { return false; }
if (!self.options.dragBoards) return false; if (!self.options.dragBoards) return false;
return (handle.classList.contains('kanban-board-header') || handle.classList.contains('kanban-title-board')); return (handle.classList.contains('kanban-board-header') || handle.classList.contains('kanban-title-board'));
}, },
accepts: function (el, target, source, sibling) { accepts: function (el, target, source, sibling) {
if (self.options.readOnly) { return false; } if (self.options.readOnly) { return false; }
return target.classList.contains('kanban-container'); return target.classList.contains('kanban-container');
}, },
revertOnSpill: true, revertOnSpill: true,
@ -145,14 +146,17 @@
//Init Drag Item //Init Drag Item
self.drake = self.dragula(self.boardContainer, { self.drake = self.dragula(self.boardContainer, {
moves: function (el, source, handle, sibling) { moves: function (el, source, handle, sibling) {
if (self.options.readOnly) { return false; } if (self.options.readOnly) { return false; }
return handle.classList.contains('kanban-item'); return handle.classList.contains('kanban-item');
}, },
accepts: function (el, target, source, sibling) { accepts: function (el, target, source, sibling) {
if (self.options.readOnly) { return false; } if (self.options.readOnly) { return false; }
return true; return true;
}, },
revertOnSpill: true revertOnSpill: true
})
.on('cancel', function(el, container, source) {
self.enableAllBoards();
}) })
.on('drag', function (el, source) { .on('drag', function (el, source) {
// we need to calculate the position before starting to drag // we need to calculate the position before starting to drag
@ -184,7 +188,9 @@
var boardId = source.parentNode.dataset.id; var boardId = source.parentNode.dataset.id;
self.options.dragcancelEl(el, boardId); self.options.dragcancelEl(el, boardId);
}) })
.on('drop', function (el, target, source, sibling) { .on('drop', function(el, target, source, sibling) {
self.enableAllBoards();
console.log("In drop"); console.log("In drop");
// TODO: update board object board order // TODO: update board object board order
@ -240,6 +246,15 @@
} }
}; };
this.enableAllBoards = function() {
var allB = document.querySelectorAll('.kanban-board');
if (allB.length > 0 && allB !== undefined) {
for (var i = 0; i < allB.length; i++) {
allB[i].classList.remove('disabled-board');
}
}
};
this.addElement = function (boardID, element) { this.addElement = function (boardID, element) {
// add Element to JSON // add Element to JSON
@ -260,6 +275,7 @@
nodeItem.dragendfn = element.dragend; nodeItem.dragendfn = element.dragend;
nodeItem.dropfn = element.drop; nodeItem.dropfn = element.drop;
__onclickHandler(nodeItem); __onclickHandler(nodeItem);
__onColorClickHandler(nodeItem, "item");
board.appendChild(nodeItem); board.appendChild(nodeItem);
// send event that board has changed // send event that board has changed
self.onChange(); self.onChange();
@ -272,8 +288,19 @@
return self; return self;
}; };
this.addBoards = function (boards) {
this.addBoards = function(boards) {
if (self.options.responsivePercentage) {
self.container.style.width = '100%';
self.options.gutter = '1%';
if (window.innerWidth > self.options.responsive) {
var boardWidth = (100 - boards.length * 2) / boards.length;
} else {
var boardWidth = 100 - (boards.length * 2);
}
} else {
var boardWidth = self.options.widthBoard; var boardWidth = self.options.widthBoard;
}
var addButton = self.options.addItemButton; var addButton = self.options.addItemButton;
var buttonContent = self.options.buttonContent; var buttonContent = self.options.buttonContent;
@ -290,7 +317,11 @@
boardNode.dataset.id = board.id; boardNode.dataset.id = board.id;
boardNode.classList.add('kanban-board'); boardNode.classList.add('kanban-board');
//set style //set style
if (self.options.responsivePercentage) {
boardNode.style.width = boardWidth + '%';
} else {
boardNode.style.width = boardWidth; boardNode.style.width = boardWidth;
}
boardNode.style.marginLeft = self.options.gutter; boardNode.style.marginLeft = self.options.gutter;
boardNode.style.marginRight = self.options.gutter; boardNode.style.marginRight = self.options.gutter;
// header board // header board
@ -303,6 +334,10 @@
headerBoard.classList.add(value); headerBoard.classList.add(value);
}); });
if (board.color !== '' && board.color !== undefined) { if (board.color !== '' && board.color !== undefined) {
headerBoard._jscLinkedInstance = undefined;
jscolorL = new jscolor(headerBoard,{valueElement:undefined});
jscolorL.fromString(board.color);
headerBoard._jscLinkedInstance = undefined;
headerBoard.classList.add("kanban-header-" + board.color); headerBoard.classList.add("kanban-header-" + board.color);
} }
titleBoard = document.createElement('div'); titleBoard = document.createElement('div');
@ -311,7 +346,7 @@
titleBoard.clickfn = board.boardTitleClick; titleBoard.clickfn = board.boardTitleClick;
__onboardTitleClickHandler(titleBoard); __onboardTitleClickHandler(titleBoard);
headerBoard.appendChild(titleBoard); headerBoard.appendChild(titleBoard);
__onColorClickHandler(headerBoard); __onColorClickHandler(headerBoard, "board");
// if add button is true, add button to the board // if add button is true, add button to the board
if (addButton) { if (addButton) {
@ -332,14 +367,24 @@
var nodeItem = document.createElement('div'); var nodeItem = document.createElement('div');
nodeItem.classList.add('kanban-item'); nodeItem.classList.add('kanban-item');
nodeItem.dataset.eid = itemKanban.id; nodeItem.dataset.eid = itemKanban.id;
nodeItem.innerHTML = itemKanban.title; var nodeItemText = document.createElement('div');
nodeItemText.classList.add('kanban-item-text');
nodeItemText.dataset.eid = itemKanban.id;
nodeItemText.innerHTML = itemKanban.title;
nodeItem.appendChild(nodeItemText);
//add function //add function
nodeItem.clickfn = itemKanban.click; nodeItemText.clickfn = itemKanban.click;
nodeItem.dragfn = itemKanban.drag; nodeItemText.dragfn = itemKanban.drag;
nodeItem.dragendfn = itemKanban.dragend; nodeItemText.dragendfn = itemKanban.dragend;
nodeItem.dropfn = itemKanban.drop; nodeItemText.dropfn = itemKanban.drop;
//add click handler of item //add click handler of item
__onclickHandler(nodeItem); __onclickHandler(nodeItemText);
if (itemKanban.color !== '' && itemKanban.color !== undefined) {
jscolorL = new jscolor(nodeItem,{valueElement:undefined});
jscolorL.fromString(itemKanban.color);
}
__onColorClickHandler(nodeItem, "item");
contentBoard.appendChild(nodeItem); contentBoard.appendChild(nodeItem);
} }
//footer board //footer board
@ -487,12 +532,10 @@
}); });
} }
function __onColorClickHandler(nodeItem, clickfn) { function __onColorClickHandler(nodeItem, type) {
nodeItem.addEventListener('click', function (e) { nodeItem.addEventListener('click', function (e) {
e.preventDefault; e.preventDefault;
self.options.colorClick(this); self.options.colorClick(this, type);
if (typeof (this.clickfn) === 'function')
this.clickfn(this);
}); });
} }

File diff suppressed because it is too large Load Diff

@ -27,6 +27,7 @@ body.cp-app-pad {
height: 100%; height: 100%;
border: 0; border: 0;
> .cke_inner { > .cke_inner {
overflow: hidden;
flex: 1; flex: 1;
position: unset; position: unset;
display: flex; display: flex;
@ -47,6 +48,20 @@ body.cp-app-pad {
display: block; display: block;
overflow-x: auto; overflow-x: auto;
max-height: 100vh; max-height: 100vh;
.cke_dialog_contents {
#ck-mediatag-preview {
margin: auto;
resize: both;
max-width: 300px;
max-height: 300px;
overflow: auto;
}
media-tag {
display: flex;
border-style: solid;
border-color: black;
}
}
} }
.cke_wysiwyg_frame { .cke_wysiwyg_frame {

@ -43,9 +43,7 @@ CKEDITOR.dialog.add('mediatag', function (editor) {
type: 'html', type: 'html',
id: 'preview', id: 'preview',
html: '<label>'+Messages.preview+'</label>'+ html: '<label>'+Messages.preview+'</label>'+
'<div id="ck-mediatag-preview"'+ '<div id="ck-mediatag-preview"></div>'
'style="margin:auto;resize:both;max-width:300px;max-height:300px;overflow:auto"'+
'></div>'
}, },
] ]
}, },
@ -77,11 +75,6 @@ CKEDITOR.dialog.add('mediatag', function (editor) {
var $preview = $(dialog).find('#ck-mediatag-preview'); var $preview = $(dialog).find('#ck-mediatag-preview');
var $clone = $(el.$).clone(); var $clone = $(el.$).clone();
$clone.css({
display: 'flex',
'border-style': 'solid',
'border-color': 'black'
});
$preview.html('').append($clone); $preview.html('').append($clone);
var center = function () { var center = function () {
@ -125,7 +118,7 @@ CKEDITOR.dialog.add('mediatag', function (editor) {
update(); update();
}); });
setTimeout(center); setTimeout(update);
}, },
onOk: function() { onOk: function() {
var dialog = this; var dialog = this;

@ -1092,6 +1092,11 @@ define([
Test(passIfOk); Test(passIfOk);
} }
// No need for onLocal in openPadChat because in poll, we listen for metadata changes
// and save them everytime.
// See `metadataMgr.onChange(function () {`
common.openPadChat(function () {});
UI.removeLoadingScreen(); UI.removeLoadingScreen();
var privateDat = metadataMgr.getPrivateData(); var privateDat = metadataMgr.getPrivateData();
var skipTemp = Util.find(privateDat, var skipTemp = Util.find(privateDat,
@ -1160,6 +1165,7 @@ define([
var configTb = { var configTb = {
displayed: [ displayed: [
'chat',
'userlist', 'userlist',
'title', 'title',
'useradmin', 'useradmin',

@ -37,7 +37,8 @@ define([
window.addEventListener('message', onMsg); window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {
SFCommonO.start({ SFCommonO.start({
useCreationScreen: true useCreationScreen: true,
messaging: true
}); });
}); });
}); });

@ -236,9 +236,9 @@ define([
label: { class: 'noTitle' } label: { class: 'noTitle' }
}); });
var $div2 = $(h('div.cp-settings-autostore-radio', [ var $div2 = $(h('div.cp-settings-autostore-radio', [
opt1, opt3,
opt2, opt2,
opt3 opt1
])).appendTo($div); ])).appendTo($div);
$div.find('input[type="radio"]').on('change', function () { $div.find('input[type="radio"]').on('change', function () {

@ -107,6 +107,7 @@
.cp-app-slide-viewer { .cp-app-slide-viewer {
width: 50vw; width: 50vw;
overflow: hidden; overflow: hidden;
z-index: 998;
div#cp-app-slide-modal:not(.cp-app-slide-shown) { div#cp-app-slide-modal:not(.cp-app-slide-shown) {
position: relative; position: relative;
top: auto; top: auto;

@ -5,7 +5,7 @@
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> <meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" /> <meta name="referrer" content="no-referrer" />
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script> <script async data-bootload="/common/sframe-app-outer.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style> <style>
html, body { html, body {
margin: 0px; margin: 0px;

@ -1,43 +0,0 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'/common/dom-ready.js',
'/common/requireconfig.js',
'/common/sframe-common-outer.js'
], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) {
var requireConfig = RequireConfig();
// Loaded in load #2
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
document.getElementById('sbox-iframe').setAttribute('src',
ApiConfig.httpSafeOrigin + '/whiteboard/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
// loading screen setup.
var done = waitFor();
var onMsg = function (msg) {
var data = JSON.parse(msg.data);
if (data.q !== 'READY') { return; }
window.removeEventListener('message', onMsg);
var _done = done;
done = function () { };
_done();
};
window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) {
SFCommonO.start({
useCreationScreen: true
});
});
});
Loading…
Cancel
Save