Merge branch 'soon'

pull/1/head
ansuz 6 years ago
commit 82b805fea6

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

@ -191,7 +191,10 @@ define([
// load the user's object using the legacy credentials
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
// 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
// it will should be pinned by anyone's drive.
if (rt.proxy[Constants.deprecatedKey]) {
waitFor.abort();
return void cb('NO_SUCH_USER', res);
}
@ -514,7 +518,22 @@ define([
if (testing) { return void proceed(result); }
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);
});
});
});
}, 500);
}, 200);

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

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

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

@ -0,0 +1,372 @@
@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%;
}
.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;
}
&: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();
};
& {
@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_line-height: 32px;
@toolbar_top-height: 64px;
@ -134,9 +146,39 @@
}
}
.cp-toolbar-userlist-drawer {
.cp-toolbar-chat-drawer {
background-color: @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;
min-width: 175px;
width: 175px;
@ -145,6 +187,7 @@
overflow-x: hidden;
padding: 10px;
box-sizing: border-box;
order: -3;
.cp-toolbar-userlist-drawer-close {
position: absolute;
margin-top: -10px;
@ -219,6 +262,14 @@
display: flex;
justify-content: space-between;
align-items: center;
button {
width: 20px;
font-size: 16px;
padding: 0;
border: none;
height: 20px;
cursor: pointer;
}
}
.cp-toolbar-userlist-name-input {
flex: 1;
@ -235,14 +286,6 @@
min-height: 0;
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 {
padding: 0;
}
@ -276,11 +319,15 @@
margin: 50px;
}
&> div {
flex: 1;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
overflow-y: auto;
}
&> div:last-child {
flex: unset;
margin: 50px 0;
}
}
@ -299,13 +346,15 @@
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 {
& > p {
display: none;
}
& > div {
align-content: unset;
align-items: center;
li {
height: 40px;
width: 200px;
@ -313,9 +362,11 @@
align-items: center;
.fa {
font-size: 32px;
min-width: 50px;
}
.cp-icons-name {
height: auto;
text-align: left;
}
}
}
@ -344,7 +395,7 @@
color: @toolbar-color;
color: var(--toolbar-color);
}
.cp-toolbar-userlist-name-edit {
.cp-toolbar-userlist-button {
color: @toolbar-userlist-name-edit;
color: var(--toolbar-userlist-name-edit);
background: transparent;
@ -961,7 +1012,8 @@
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-spinner { order: 3; }
@ -969,6 +1021,11 @@
width: 125px;
text-align: center;
}
#cp-toolbar-chat-drawer-open button {
&.cp-toolbar-notification {
animation: notification 2s ease-in-out infinite;
}
}
.cp-toolbar-share-button {
width: 50px;
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) {
.container {
padding-left: 0;

@ -135,6 +135,8 @@ define(function () {
out.userListButton = "Liste d'utilisateurs";
out.chatButton = "Chat";
out.userAccountButton = "Votre compte";
out.newButton = 'Nouveau';
@ -363,7 +365,8 @@ define(function () {
out.contacts_remove = 'Supprimer ce contact';
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_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_info2 = "Cliquer sur le nom d'un contact pour discuter avec lui";
@ -375,6 +378,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_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
out.fm_rootName = "Documents";
@ -407,6 +416,7 @@ define(function () {
out.fm_noname = "Document sans titre";
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_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_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 ?";
@ -1204,5 +1214,16 @@ define(function () {
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;
});

@ -136,6 +136,8 @@ define(function () {
out.userListButton = "User list";
out.chatButton = "Chat";
out.userAccountButton = "Your account";
out.newButton = 'New';
@ -365,6 +367,8 @@ define(function () {
out.contacts_remove = 'Remove this contact';
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_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_info2 = "Click your contact's icon to chat with them";
@ -376,6 +380,12 @@ define(function () {
out.contacts_removeHistoryServerError = 'There was an error while removing your chat history. Try again later';
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
out.fm_rootName = "Documents";
@ -407,12 +417,13 @@ define(function () {
out.fm_openParent = "Show in folder";
out.fm_noname = "Untitled Document";
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_removePermanentlyDialog = "Are you sure you want to remove that element from your CryptDrive permanently?";
out.fm_removeSeveralPermanentlyDialog = "Are you sure you want to permanently remove these {0} elements from your CryptDrive?";
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_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_deleteOwnedPads = "Are you sure you want to remove permanently these pads 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 permanently remove these pads from the server?";
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_contextMenuError = "Unable to open the context menu for that element. If the problem persist, try to reload the page.";
@ -1253,5 +1264,16 @@ define(function () {
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
// 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;
});

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "2.7.0",
"version": "2.8.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",

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

@ -13,6 +13,8 @@ define(function () {
storageKey: 'filesData',
tokenKey: 'loginToken',
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:40px;" }),
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),
Pages.setHTML(h('div.cp-corner-footer'), footer)
]);

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

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

@ -12,10 +12,11 @@ define([
'/common/clipboard.js',
'/customize/messages.js',
'/customize/application_config.js',
'/customize/pages.js',
'/bower_components/nthen/index.js',
'css!/customize/fonts/cptools/style.css'
], function ($, Config, Util, Hash, Language, UI, Constants, Feedback, h, MediaTag, Clipboard,
Messages, AppConfig, NThen) {
Messages, AppConfig, Pages, NThen) {
var UIElements = {};
// Configure MediaTags to use our local viewer
@ -630,23 +631,25 @@ define([
if (!data.FM) { return; }
var $input = $('<input>', {
'type': 'file',
'style': 'display: none;'
'style': 'display: none;',
'multiple': 'multiple'
}).on('change', function (e) {
var file = e.target.files[0];
var ev = {
target: data.target
};
if (data.filter && !data.filter(file)) {
return;
}
if (data.transformer) {
data.transformer(file, function (newFile) {
data.FM.handleFile(newFile, ev);
if (callback) { callback(); }
});
return;
}
data.FM.handleFile(file, ev);
var files = Util.slice(e.target.files);
files.forEach(function (file) {
var ev = {
target: data.target
};
if (data.filter && !data.filter(file)) {
return;
}
if (data.transformer) {
data.transformer(file, function (newFile) {
data.FM.handleFile(newFile, ev);
});
return;
}
data.FM.handleFile(file, ev);
});
if (callback) { callback(); }
});
if (data.accept) { $input.attr('accept', data.accept); }
@ -1779,13 +1782,16 @@ define([
var $container = $('<div>');
var i = 0;
AppConfig.availablePadTypes.forEach(function (p) {
var types = AppConfig.availablePadTypes.filter(function (p) {
if (p === 'drive') { return; }
if (p === 'contacts') { return; }
if (p === 'todo') { return; }
if (p === 'file') { return; }
if (!common.isLoggedIn() && AppConfig.registeredOnlyTypes &&
AppConfig.registeredOnlyTypes.indexOf(p) !== -1) { return; }
return true;
});
types.forEach(function (p) {
var $element = $('<li>', {
'class': 'cp-icons-element',
'id': 'cp-newpad-icons-'+ (i++)
@ -1809,7 +1815,7 @@ define([
var selected = -1;
var next = function () {
selected = ++selected % 5;
selected = ++selected % types.length;
$container.find('.cp-icons-element-selected').removeClass('cp-icons-element-selected');
$container.find('#cp-newpad-icons-'+selected).addClass('cp-icons-element-selected');
};
@ -2325,6 +2331,52 @@ define([
$(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;
UIElements.displayStorePadPopup = function (common, data) {
if (storePopupState) { return; }
@ -2347,15 +2399,20 @@ define([
});
$(hide).click(function () {
UIElements.displayCrowdfunding(common);
modal.delete();
});
$(store).click(function () {
modal.delete();
common.getSframeChannel().query("Q_AUTOSTORE_STORE", null, function (err, obj) {
if (err || (obj && obj.error)) {
console.error(err || obj.error);
var error = err || (obj && obj.error);
if (error) {
if (error === 'E_OVER_LIMIT') {
return void UI.warn(Messages.pinLimitReached);
}
return void UI.warn(Messages.autostore_error);
}
modal.delete();
UIElements.displayCrowdfunding(common);
UI.log(Messages.autostore_saved);
});
});

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

@ -25,9 +25,11 @@ define([
// Tasks list
var checkedTaskItemPtn = /^\s*(<p>)?\[[xX]\](<\/p>)?\s*/;
var uncheckedTaskItemPtn = /^\s*(<p>)?\[ ?\](<\/p>)?\s*/;
var bogusCheckPtn = /<input( checked=""){0,1} disabled="" type="checkbox">/;
renderer.listitem = function (text) {
var isCheckedTaskItem = checkedTaskItemPtn.test(text);
var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text);
var hasBogusInput = bogusCheckPtn.test(text);
if (isCheckedTaskItem) {
text = text.replace(checkedTaskItemPtn,
'<i class="fa fa-check-square" aria-hidden="true"></i>&nbsp;') + '\n';
@ -36,6 +38,15 @@ define([
text = text.replace(uncheckedTaskItemPtn,
'<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"' : '';
return '<li'+ cls + '>' + text + '</li>\n';
};

@ -59,6 +59,9 @@ define([
obj[key] = data.value;
}
broadcast([clientId], "UPDATE_METADATA");
if (Array.isArray(path) && path[0] === 'profile' && store.messenger) {
store.messenger.updateMyData();
}
onSync(cb);
};
@ -462,7 +465,7 @@ define([
if (data.password) { pad.password = data.password; }
if (data.channel) { pad.channel = data.channel; }
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', {
path: ['drive', UserObject.FILES_DATA]
}, clientId);
@ -597,6 +600,7 @@ define([
Store.setDisplayName = function (clientId, value, cb) {
store.proxy[Constants.displayNameKey] = value;
broadcast([clientId], "UPDATE_METADATA");
if (store.messenger) { store.messenger.updateMyData(); }
onSync(cb);
};
@ -859,6 +863,9 @@ define([
},
pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
friendComplete: function (data) {
if (data.friend && store.messenger && store.messenger.onFriendAdded) {
store.messenger.onFriendAdded(data.friend);
}
postMessage(clientId, "EV_FRIEND_COMPLETE", data);
},
friendRequest: function (data, cb) {
@ -892,6 +899,7 @@ define([
Store.messenger = {
getFriendList: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.getFriendList(function (e, keys) {
cb({
error: e,
@ -900,6 +908,7 @@ define([
});
},
getMyInfo: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.getMyInfo(function (e, info) {
cb({
error: e,
@ -908,6 +917,7 @@ define([
});
},
getFriendInfo: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.getFriendInfo(data, function (e, info) {
cb({
error: e,
@ -916,6 +926,7 @@ define([
});
},
removeFriend: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.removeFriend(data, function (e, info) {
cb({
error: e,
@ -924,11 +935,13 @@ define([
});
},
openFriendChannel: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.openFriendChannel(data, function (e) {
cb({ error: e, });
});
},
getFriendStatus: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.getStatus(data, function (e, online) {
cb({
error: e,
@ -937,6 +950,7 @@ define([
});
},
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) {
cb({
error: e,
@ -945,6 +959,7 @@ define([
});
},
sendMessage: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.sendMessage(data.curvePublic, data.content, function (e) {
cb({
error: e,
@ -952,11 +967,17 @@ define([
});
},
setChannelHead: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.setChannelHead(data.curvePublic, data.sig, function (e) {
cb({
error: e
});
});
},
execCommand: function (clientId, data, cb) {
if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); }
store.messenger.execCommand(data, cb);
}
};
@ -1317,7 +1338,6 @@ define([
}
};
var messengerEventInit = false;
var sendMessengerEvent = function (q, data) {
messengerEventClients.forEach(function (cId) {
postMessage(cId, q, data);
@ -1327,41 +1347,49 @@ define([
if (messengerEventClients.indexOf(clientId) === -1) {
messengerEventClients.push(clientId);
}
if (!messengerEventInit) {
var messenger = store.messenger = Messenger.messenger(store);
messenger.on('message', function (message) {
sendMessengerEvent('CONTACTS_MESSAGE', message);
};
var loadMessenger = function () {
if (AppConfig.availablePadTypes.indexOf('contacts') === -1) { return; }
var messenger = store.messenger = Messenger.messenger(store);
messenger.on('message', function (message) {
sendMessengerEvent('CONTACTS_MESSAGE', message);
});
messenger.on('join', function (curvePublic, channel) {
sendMessengerEvent('CONTACTS_JOIN', {
curvePublic: curvePublic,
channel: channel,
});
messenger.on('join', function (curvePublic, channel) {
sendMessengerEvent('CONTACTS_JOIN', {
curvePublic: curvePublic,
channel: channel,
});
});
messenger.on('leave', function (curvePublic, channel) {
sendMessengerEvent('CONTACTS_LEAVE', {
curvePublic: curvePublic,
channel: channel,
});
messenger.on('leave', function (curvePublic, channel) {
sendMessengerEvent('CONTACTS_LEAVE', {
curvePublic: curvePublic,
channel: channel,
});
});
messenger.on('update', function (info, types, channel) {
sendMessengerEvent('CONTACTS_UPDATE', {
types: types,
info: info,
channel: channel
});
messenger.on('update', function (info, curvePublic) {
sendMessengerEvent('CONTACTS_UPDATE', {
curvePublic: curvePublic,
info: info,
});
});
messenger.on('friend', function (curvePublic) {
sendMessengerEvent('CONTACTS_FRIEND', {
curvePublic: curvePublic,
});
messenger.on('friend', function (curvePublic) {
sendMessengerEvent('CONTACTS_FRIEND', {
curvePublic: curvePublic,
});
});
messenger.on('unfriend', function (curvePublic, removedByMe) {
sendMessengerEvent('CONTACTS_UNFRIEND', {
curvePublic: curvePublic,
removedByMe: removedByMe
});
messenger.on('unfriend', function (curvePublic) {
sendMessengerEvent('CONTACTS_UNFRIEND', {
curvePublic: curvePublic,
});
});
messenger.on('event', function (ev, data) {
sendMessengerEvent('CHAT_EVENT', {
ev: ev,
data: data
});
messengerEventInit = true;
}
});
};
@ -1450,6 +1478,7 @@ define([
});
userObject.fixFiles();
loadSharedFolders(waitFor);
loadMessenger();
}).nThen(function () {
var requestLogin = function () {
broadcast([], "REQUEST_LOGIN");

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

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

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

@ -262,6 +262,7 @@ define([
donateURL: Cryptpad.donateURL,
upgradeURL: Cryptpad.upgradeURL
},
plan: localStorage[Utils.Constants.plan],
isNewFile: isNewFile,
isDeleted: isNewFile && window.location.hash.length > 0,
forceCreationScreen: forceCreationScreen,
@ -370,7 +371,7 @@ define([
forceSave: true
};
Cryptpad.setPadTitle(data, function (err) {
cb(err);
cb({error: err});
});
});
sframeChan.on('Q_IS_PAD_STORED', function (data, cb) {
@ -776,6 +777,22 @@ define([
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) {
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
funcs.initCodeMirrorApp = callWithCommon(CodeMirror.create);
@ -517,6 +535,11 @@ define([
UI.alert(Messages.chrome68);
});
funcs.isPadStored(function (err, val) {
if (err || !val) { return; }
UIElements.displayCrowdfunding(funcs);
});
ctx.sframeChan.ready();
cb(funcs);
});

@ -35,7 +35,7 @@ define([], function () {
});
sFrameChan.on('EV_CONTACTS_UPDATE', function (data) {
_handlers.update.forEach(function (f) {
f(data.info, data.curvePublic);
f(data.info, data.types, data.channel);
});
});
sFrameChan.on('EV_CONTACTS_FRIEND', function (data) {
@ -45,7 +45,7 @@ define([], function () {
});
sFrameChan.on('EV_CONTACTS_UNFRIEND', function (data) {
_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_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.
'EV_LOCALSTORE_PUT': true,
// Put one entry in the parent sessionStorage
@ -218,6 +223,8 @@ define({
// Refresh the drive when the drive has changed ('change' or 'remove' events)
'EV_DRIVE_CHANGE': 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
'Q_REMOVE_OWNED_CHANNEL': true,

@ -6,8 +6,11 @@ define([
'/common/common-interface.js',
'/common/common-hash.js',
'/common/common-feedback.js',
'/common/sframe-messenger-inner.js',
'/contacts/messenger-ui.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 Bar = {
@ -231,16 +234,15 @@ define([
var name = data.name || Messages.anonymous;
var $span = $('<span>', {'class': 'cp-avatar'});
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 $nameValue = $('<span>', {
'class': 'cp-toolbar-userlist-name-value'
}).text(name).appendTo($nameSpan);
var isMe = data.uid === user.uid;
if (isMe && !priv.readOnly) {
$nameSpan.html('');
var $nameValue = $('<span>', {
'class': 'cp-toolbar-userlist-name-value'
}).text(name).appendTo($nameSpan);
if (!Config.disableProfile) {
var $button = $('<button>', {
'class': 'fa fa-pencil cp-toolbar-userlist-name-edit',
'class': 'fa fa-pencil cp-toolbar-userlist-button',
title: Messages.user_rename
}).appendTo($nameSpan);
$button.hover(function (e) { e.preventDefault(); e.stopPropagation(); });
@ -296,16 +298,24 @@ define([
$('<span>', {'class': 'cp-toolbar-userlist-friend'}).text(Messages.userlist_pending)
.appendTo($rightCol);
} else {
$('<span>', {
'class': 'fa fa-user-plus cp-toolbar-userlist-friend',
$('<button>', {
'class': 'fa fa-user-plus cp-toolbar-userlist-button',
'title': Messages._getKey('userlist_addAsFriendTitle', [
name
])
}).appendTo($rightCol).click(function (e) {
}).appendTo($nameSpan).click(function (e) {
e.stopPropagation();
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) {
$span.addClass('cp-userlist-clickable');
@ -410,6 +420,77 @@ define([
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) {
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the userlist");
@ -997,6 +1078,7 @@ define([
// Create the subelements
var tb = {};
tb['userlist'] = createUserList;
tb['chat'] = createChat;
tb['share'] = createShare;
tb['fileshare'] = createFileShare;
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/messenger.less';
// body
&.cp-app-contacts {
@ -9,238 +9,14 @@
@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;
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 {
display: flex; // We need this to remove a 3px border at the bottom of the toolbar
}
#cp-app-contacts-friendlist {
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%);
}
}
}
}
.messenger_main();
}

@ -41,22 +41,7 @@ define([
document.body.appendChild(toolbarElement);
var messaging = h('div#cp-app-contacts-messaging', [
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,
]);
var appElement = h('div#cp-app-contacts-container');
document.body.appendChild(appElement);
@ -73,7 +58,7 @@ define([
var messenger = Messenger.create(sFrameChan);
MessengerUI.create(messenger, $(friendList), $(messaging), common);
MessengerUI.create(messenger, $(appElement), common);
UI.removeLoadingScreen();

@ -3,58 +3,140 @@ define([
'/customize/messages.js',
'/common/common-util.js',
'/common/common-interface.js',
'/common/common-notifier.js',
'/common/hyperscript.js',
'/bower_components/marked/marked.min.js',
'/common/media-tag.js',
], function ($, Messages, Util, UI, Notifier, h, Marked, MediaTag) {
], function ($, Messages, Util, UI, h, Marked, MediaTag) {
'use strict';
var debug = console.log;
debug = function () {};
var MessengerUI = {};
var dataQuery = function (curvePublic) {
return '[data-key="' + curvePublic + '"]';
var dataQuery = function (id) {
return '[data-key="' + id + '"]';
};
var userQuery = function (curve) {
return '[data-user="' + curve + '"]';
};
var initChannel = function (state, curvePublic, info) {
console.log('initializing channel for [%s]', curvePublic);
state.channels[curvePublic] = {
messages: [],
HEAD: info.lastKnownHash,
TAIL: null,
var initChannel = function (state, info) {
console.log('initializing channel for [%s]', info.id);
var h, t;
if (Array.isArray(info.messages) && info.messages.length) {
h = info.messages[info.messages.length -1].sig;
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) {
var origin = common.getMetadataMgr().getPrivateData().origin;
MessengerUI.create = function (messenger, $container, common, toolbar) {
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 = {
active: '',
channels: {}
};
state.channels = {};
var displayNames = state.displayNames = {};
var contactsData = state.contactsData = {};
var avatars = state.avatars = {};
var setActive = function (curvePublic) {
state.active = curvePublic;
var setActive = function (id) {
state.active = id;
};
var isActive = function (curvePublic) {
return curvePublic === state.active;
var isActive = function (id) {
return id === state.active;
};
var find = {};
find.inList = function (curvePublic) {
return $userlist.find(dataQuery(curvePublic));
find.inList = function (id) {
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) {
find.inList(curvePublic).addClass('cp-app-contacts-notify');
var onResize = function () {
// 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) {
find.inList(curvePublic).removeClass('cp-app-contacts-notify');
var reorderRooms = function () {
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();
};
$(window).on('resize', onResize);
var m = function (md) {
var d = h('div.cp-app-contacts-content');
try {
@ -82,25 +164,36 @@ define([
var markup = {};
markup.message = function (msg) {
if (msg.type !== 'MSG') { return; }
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', {
title: msg.time? new Date(msg.time).toLocaleString(): '?',
'data-key': curvePublic,
//title: time || '?',
'data-user': curvePublic,
'data-day': day
}, [
name? h('div.cp-app-contacts-sender', name): undefined,
name? h('div.cp-app-contacts-sender', [
h('span.cp-app-contacts-sender-name', name),
h('span.cp-app-contacts-sender-time', day)
]): undefined,
m(msg.text),
h('div.cp-app-contacts-time', hour)
]);
};
var getChat = function (curvePublic) {
return $messages.find(dataQuery(curvePublic));
var getChat = function (id) {
return $messages.find(dataQuery(id));
};
var normalizeLabels = function ($messagebox) {
$messagebox.find('div.cp-app-contacts-message').toArray().reduce(function (a, 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();
return a;
}
@ -108,29 +201,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', {
title: Messages.contacts_fetchHistory,
});
var displayName = data.displayName;
var chan = state.channels[id];
var displayName = chan.name;
var fetching = false;
var $moreHistory = $(moreHistory).click(function () {
if (fetching) { return; }
// get oldest known message...
var channel = state.channels[curvePublic];
var channel = state.channels[id];
if (channel.exhausted) {
return void $moreHistory.addClass('cp-app-contacts-faded');
}
console.log('getting history');
debug('getting history');
var sig = channel.TAIL || channel.HEAD;
fetching = true;
var $messagebox = $(getChat(curvePublic)).find('.cp-app-contacts-messages');
messenger.getMoreHistory(curvePublic, sig, 10, function (e, history) {
var $messagebox = $(getChat(id)).find('.cp-app-contacts-messages');
messenger.getMoreHistory(id, sig, 10, function (e, history) {
fetching = false;
if (e) { return void console.error(e); }
@ -139,13 +234,16 @@ define([
return;
}
history.forEach(function (msg) {
history.forEach(function (msg, i) {
if (channel.exhausted) { return; }
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) {
console.error('No more messages to fetch');
channel.exhausted = true;
console.log(channel);
return void $moreHistory.addClass('cp-app-contacts-faded');
} else {
channel.TAIL = msg.sig;
@ -156,9 +254,11 @@ define([
if (msg.type !== 'MSG') { return; }
// FIXME Schlameil the painter (performance does not scale well)
// XXX trust the server?
/*
if (channel.messages.some(function (old) {
return msg.sig === old.sig;
})) { return; }
})) { return; }*/
channel.messages.unshift(msg);
var el_message = markup.message(msg);
@ -176,22 +276,43 @@ define([
UI.confirm(Messages.contacts_confirmRemoveHistory, function (yes) {
if (!yes) { return; }
messenger.clearOwnedChannel(data.channel, function (e) {
messenger.clearOwnedChannel(id, function (e) {
if (e) {
console.error(e);
UI.alert(Messages.contacts_removeHistoryServerError);
return;
}
// XXX clear the UI?
});
});
});
var avatar = h('div.cp-avatar');
var header = h('div.cp-app-contacts-header', [
avatar,
moreHistory,
removeHistory,
]);
var headerContent = [avatar, moreHistory, data.isFriendCHat ? removeHistory : undefined];
if (isApp) {
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 input = h('textarea', {
placeholder: Messages.contacts_typeHere
@ -205,12 +326,13 @@ define([
]);
var $avatar = $(avatar);
if (data.avatar && avatars[data.avatar]) {
$avatar.append(avatars[data.avatar]).append(rightCol);
var friend = contactsData[curvePublic] || {};
if (friend.avatar && avatars[friend.avatar]) {
$avatar.append(avatars[friend.avatar]).append(rightCol);
} else {
common.displayAvatar($avatar, data.avatar, data.displayName, function ($img) {
if (data.avatar && $img) {
avatars[data.avatar] = $img[0].outerHTML;
common.displayAvatar($avatar, friend.avatar, displayName, function ($img) {
if (friend.avatar && $img) {
avatars[friend.avatar] = $img[0].outerHTML;
}
$(rightCol).insertAfter($avatar);
});
@ -221,14 +343,14 @@ define([
if (typeof(content) !== 'string' || !content.trim()) { return; }
if (sending) { return false; }
sending = true;
messenger.sendMessage(curvePublic, content, function (e) {
messenger.sendMessage(id, content, function (e) {
if (e) {
// failed to send
return void console.error('failed to send');
}
input.value = '';
sending = false;
console.log('sent successfully');
debug('sent successfully');
var $messagebox = $(messages);
var height = $messagebox[0].scrollHeight;
@ -268,9 +390,11 @@ define([
$(sendButton).click(function () { send(input.value); });
return h('div.cp-app-contacts-chat', {
'data-key': curvePublic,
'data-key': id,
'data-user': data.isFriendChat && curvePublic
}, [
header,
tips,
messages,
h('div.cp-app-contacts-input', [
input,
@ -283,16 +407,20 @@ define([
$messages.find('.cp-app-contacts-info').hide();
};
var updateStatus = function (curvePublic) {
var $status = find.inList(curvePublic).find('.cp-app-contacts-status');
// FIXME this stopped working :(
messenger.getStatus(curvePublic, function (e, online) {
var showInfo = function () {
$messages.find('.cp-app-contacts-info').show();
};
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 (e) {
find.inList(curvePublic).hide();
getChat(curvePublic).hide();
find.inList(id).hide();
getChat(id).hide();
return void console.error(curvePublic, e);
return void console.error(id, e);
}
if (online) {
return void $status
@ -302,72 +430,81 @@ define([
});
};
var display = function (curvePublic) {
var channel = state.channels[curvePublic];
var display = function (chanId) {
var channel = state.channels[chanId];
var lastMsg = channel.messages.slice(-1)[0];
if (lastMsg) {
channel.HEAD = lastMsg.sig;
messenger.setChannelHead(curvePublic, channel.HEAD, function (e) {
messenger.setChannelHead(chanId, channel.HEAD, function (e) {
if (e) { console.error(e); }
});
}
setActive(curvePublic);
unnotify(curvePublic);
var $chat = getChat(curvePublic);
setActive(chanId);
unnotify(chanId);
var $chat = getChat(chanId);
hideInfo();
$messages.find('div.cp-app-contacts-chat[data-key]').hide();
if ($chat.length) {
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');
$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) {
messenger.removeFriend(curvePublic, function (e /*, removed */) {
if (e) { return void console.error(e); }
find.inList(curvePublic).remove();
//console.log(removed);
});
};
markup.friend = function (data) {
var curvePublic = data.curvePublic;
var friend = h('div.cp-app-contacts-friend.cp-avatar', {
'data-key': curvePublic,
markup.room = function (id, room, userlist) {
var roomEl = h('div.cp-app-contacts-friend.cp-avatar', {
'data-key': id,
'data-user': room.isFriendChat ? userlist[0].curvePublic : '',
title: room.name
});
var remove = h('span.cp-app-contacts-remove.fa.fa-user-times', {
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', [
h('span.cp-app-contacts-name', [data.displayName]),
remove,
h('span.cp-app-contacts-name', [room.name]),
room.isFriendChat ? remove :
room.isPadChat ? undefined : leaveRoom,
]);
var $friend = $(friend)
.click(function () {
display(curvePublic);
})
.dblclick(function () {
if (data.profile) { window.open(origin + '/profile/#' + data.profile); }
var friendData = room.isFriendChat ? userlist[0] : {};
var $room = $(roomEl).click(function () {
display(id);
}).dblclick(function () {
if (friendData.profile) { window.open(origin + '/profile/#' + friendData.profile); }
});
$(remove).click(function (e) {
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', [
Util.fixHTML(data.displayName)
Util.fixHTML(friend.name)
]), function (yes) {
if (!yes) { return; }
removeFriend(curvePublic, function (e) {
@ -379,41 +516,42 @@ define([
}, undefined, true);
});
if (data.avatar && avatars[data.avatar]) {
$friend.append(avatars[data.avatar]);
$friend.append(rightCol);
if (friendData.avatar && avatars[friendData.avatar]) {
$room.append(avatars[friendData.avatar]);
$room.append(rightCol);
} else {
common.displayAvatar($friend, data.avatar, data.displayName, function ($img) {
if (data.avatar && $img) {
avatars[data.avatar] = $img[0].outerHTML;
common.displayAvatar($room, friendData.avatar, room.name, function ($img) {
if (friendData.avatar && $img) {
avatars[friendData.avatar] = $img[0].outerHTML;
}
$friend.append(rightCol);
$room.append(rightCol);
});
}
$friend.append(status);
return $friend;
$room.append(status);
return $room;
};
var isBottomedOut = function ($elem) {
return ($elem[0].scrollHeight - $elem.scrollTop() === $elem.outerHeight());
};
var initializing = true;
messenger.on('message', function (message) {
if (!initializing) { Notifier.notify(); }
var curvePublic = message.curve;
var chanId = message.channel;
var channel = state.channels[chanId];
if (!channel) { return; }
var name = displayNames[curvePublic];
var chat = getChat(curvePublic, name);
var chat = getChat(chanId);
console.log(message);
debug(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) {
console.error("Got a message but the chat isn't open");
}
@ -427,111 +565,266 @@ define([
$messagebox.scrollTop($messagebox.outerHeight());
}
normalizeLabels($messagebox);
reorderRooms();
var channel = state.channels[curvePublic];
if (!channel) {
console.error('expected channel [%s] to be open', curvePublic);
return;
}
if (isActive(curvePublic)) {
if (isActive(chanId)) {
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); }
});
return;
}
var lastMsg = channel.messages.slice(-1)[0];
if (lastMsg.sig !== channel.HEAD) {
return void notify(curvePublic);
return void notify(chanId);
}
unnotify(curvePublic);
unnotify(chanId);
});
messenger.on('join', function (curvePublic, channel) {
channel = channel;
updateStatus(curvePublic);
messenger.on('join', function (data, channel) {
if (data.curvePublic) {
contactsData[data.curvePublic] = data;
}
updateStatus(channel);
// TODO room refresh online userlist
});
messenger.on('leave', function (curvePublic, channel) {
channel = channel;
updateStatus(curvePublic);
messenger.on('leave', function (data, channel) {
if (contactsData[data.curvePublic]) {
delete contactsData[data.curvePublic];
}
updateStatus(channel);
// TODO room refresh online userlist
});
// change in your friend list
messenger.on('update', function (info, curvePublic) {
var name = displayNames[curvePublic] = info.displayName;
messenger.on('update', function (info, types, channel) {
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
$userlist.find(userQuery(curvePublic)).find('.cp-app-contacts-name').text(name);
$userlist.find(userQuery(curvePublic)).attr('title', name);
// update title bar and messages
$messages.find(userQuery(curvePublic) + ' .cp-app-contacts-header ' +
'.cp-app-contacts-name, div.cp-app-contacts-message'+
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); }
});
}
// update label in friend list
find.inList(curvePublic).find('.cp-app-contacts-name').text(name);
if (types.indexOf('avatar') !== -1) {
var $mAvatar = $messages
.find(userQuery(curvePublic) +' .cp-app-contacts-header .cp-avatar');
var $lAvatar = $userlist.find(userQuery(curvePublic));
$lAvatar.find('.cp-avatar-default, media-tag').remove();
// update title bar and messages
$messages.find(dataQuery(curvePublic) + ' .cp-app-contacts-header ' +
'.cp-app-contacts-name, div.cp-app-contacts-message'+
dataQuery(curvePublic) + ' div.cp-app-contacts-sender').text(name).text(name);
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 connectToFriend = function (curvePublic, cb) {
messenger.getFriendInfo(curvePublic, function (e, info) {
if (e) { return void console.error(e); }
var name = displayNames[curvePublic] = info.displayName;
initChannel(state, curvePublic, info);
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(curvePublic, info);
var chatbox = markup.chatbox(id, room, friend.curvePublic);
$(chatbox).hide();
$messages.append(chatbox);
var friend = markup.friend(info, name);
$userlist.append(friend);
messenger.openFriendChannel(curvePublic, function (e) {
if (e) { return void console.error(e); }
cb();
updateStatus(curvePublic);
// don't add friends that are already in your userlist
//if (friendExistsInUserList(k)) { return; }
var $messagebox = $(chatbox).find('.cp-app-contacts-messages');
room.messages.forEach(function (msg) {
var el_message = markup.message(msg);
$messagebox.append(el_message);
});
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) {
console.log('new friend: ', curvePublic);
//console.error("TODO redraw user list");
//console.error("TODO connect to new friend");
// FIXME this doesn't work right now because the friend hasn't been fully added?
connectToFriend(curvePublic, function () {
//console.error('connected');
if (isApp) { return; }
debug('new friend: ', curvePublic);
execCommand('GET_ROOMS', {curvePublic: curvePublic}, function (err, rooms) {
if (err) { return void console.error(err); }
debug('rooms: ' + JSON.stringify(rooms));
rooms.forEach(initializeRoom);
});
});
messenger.on('unfriend', function (curvePublic) {
console.log('unfriend', curvePublic);
find.inList(curvePublic).remove();
console.error('TODO remove chatbox');
console.error('TODO show something if that chatbox was active');
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
}
});
common.getMetadataMgr().onChange(function () {
//messenger.checkNewFriends();
messenger.updateMyData();
common.getMetadataMgr().onTitleChange(function () {
var padChat = common.getPadChat();
var md = common.getMetadataMgr().getMetadata();
var name = md.title || md.defaultTitle;
$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();
var $div = $('<div>');
common.displayAvatar($div, null, name, function () {
$mAvatar.html($div.html());
$lAvatar.find('.cp-app-contacts-right-col').before($div.html());
});
});
// FIXME dirty hack
// TODO room
// messenger.on('joinroom', function (chanid))
// messenger.on('leaveroom', function (chanid))
messenger.getMyInfo(function (e, info) {
displayNames[info.curvePublic] = info.displayName;
contactsData[info.curvePublic] = info;
});
messenger.getFriendList(function (e, keys) {
var count = keys.length + 1;
var ready = function () {
count--;
if (count === 0) {
initializing = false;
UI.removeLoadingScreen();
var ready = false;
var onMessengerReady = function () {
if (isApp) { return; }
if (ready) { return; }
ready = true;
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');
}
};
ready();
keys.forEach(function (curvePublic) {
connectToFriend(curvePublic, ready);
var room = rooms[0];
var md = common.getMetadataMgr().getMetadata();
var name = md.title || md.defaultTitle;
room.name = name;
rooms.forEach(initializeRoom);
});
};
var onDisconnect = function () {
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 {
.drive_fileIcon;
border: 1px solid @colortheme_modal-fg;
li:not(.cp-app-drive-element-selected):hover {
border: 1px solid white;
background: @colortheme_modal-fg;
color: @colortheme_modal-bg;
}
.cp-modal {
display: flex;
@ -663,14 +665,16 @@
cursor: pointer;
}
&> p {
margin: 50px;
display: flex;
align-items: center;
justify-content: center;
}
&> div {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
overflow-y: auto;
flex: 1;
.cp-app-drive-new-upload {
break-after: always;
page-break-after: always;
@ -682,23 +686,27 @@
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 {
& > p {
display: none;
}
& > div {
align-content: unset;
li {
height: 40px;
width: 90%;
display: flex;
align-items: center;
align-content: unset;
.fa {
font-size: 32px;
min-width: 50px;
}
.cp-app-drive-new-name {
height: auto;
overflow: hidden;
text-overflow: ellipsis;
}
}
}

@ -1207,15 +1207,23 @@ define([
if (paths) {
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]);
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) {
$(window).focus();
if (!res) { return; }
manager.delete(pathsList, refresh);
});
}, null, true);
};
// Drag & drop:
// The data transferred is a stringified JSON containing the path of the dragged element
@ -1290,6 +1298,7 @@ define([
if (!oldPaths) { return; }
// A moved element should be removed from its previous location
var movedPaths = [];
var sharedF = false;
oldPaths.forEach(function (p) {
movedPaths.push(p.path);
@ -1301,9 +1310,25 @@ define([
var newPath = findDropPath(ev.target);
if (!newPath) { return; }
if (sharedF && manager.isPathIn(newPath, [TRASH])) {
deletePaths(null, movedPaths);
return;
return void deletePaths(null, movedPaths);
}
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) {
moveElements(movedPaths, newPath, null, refresh);
}
@ -1805,14 +1830,17 @@ define([
.click(function () {
var $input = $('<input>', {
'type': 'file',
'style': 'display: none;'
'style': 'display: none;',
'multiple': 'multiple'
}).on('change', function (e) {
var file = e.target.files[0];
var ev = {
target: $content[0],
path: findDropPath($content[0])
};
APP.FM.handleFile(file, ev);
var files = Util.slice(e.target.files);
files.forEach(function (file) {
var ev = {
target: $content[0],
path: findDropPath($content[0])
};
APP.FM.handleFile(file, ev);
});
});
$input.click();
});
@ -1889,9 +1917,11 @@ define([
var createShareButton = function (id, $container) {
var $shareBlock = $('<button>', {
'class': 'fa fa-shhare-alt cp-toolbar-share-button',
'class': 'cp-toolbar-share-button',
title: Messages.shareButton
});
$sharedIcon.clone().appendTo($shareBlock);
$('<span>').text(Messages.shareButton).appendTo($shareBlock);
var data = manager.getSharedFolderData(id);
var parsed = Hash.parsePadUrl(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'));
var sfId = manager.isInSharedFolder(currentPath);
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'));
} else {
sframeChan.event('EV_DRIVE_SET_HASH', '');
}
createTitle($toolbar.find('.cp-app-drive-path'), path);
@ -2721,7 +2756,7 @@ define([
}
var dataPath = isSharedFolder ? path.slice(0, -1) : path;
$elementRow.data('path', dataPath);
addDragAndDropHandlers($elementRow, path, true, droppable);
addDragAndDropHandlers($elementRow, dataPath, true, droppable);
if (active) {
$elementRow.addClass('cp-app-drive-element-active cp-leftside-active');
}
@ -3221,16 +3256,23 @@ define([
paths.push($(elmt).data('path'));
});
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,
// delete permanently
// Or if we are in a shared folder
// Or if the selection is only shared folders
if (!APP.loggedIn || isTrash || manager.isInSharedFolder(currentPath)
|| e.shiftKey) {
|| e.shiftKey || !paths2.length) {
deletePaths(null, paths);
return;
}
// else move to trash
moveElements(paths, [TRASH], false, refresh);
moveElements(paths2, [TRASH], false, refresh);
return;
}
});

@ -87,6 +87,14 @@ define([
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 () {
sframeChan.event('EV_NETWORK_DISCONNECT');
});

@ -67,7 +67,6 @@
text-overflow: ellipsis;
}
#kanban-edit {
color: black;
font-weight: bold;
}
}
@ -77,6 +76,7 @@
width: 100%;
background: transparent;
border: 1px solid rgba(0,0,0,0.3);
color: inherit;
}
@button-size: 50px;

@ -27,6 +27,7 @@ body.cp-app-pad {
height: 100%;
border: 0;
> .cke_inner {
overflow: hidden;
flex: 1;
position: unset;
display: flex;

@ -1092,6 +1092,11 @@ define([
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();
var privateDat = metadataMgr.getPrivateData();
var skipTemp = Util.find(privateDat,
@ -1160,6 +1165,7 @@ define([
var configTb = {
displayed: [
'chat',
'userlist',
'title',
'useradmin',

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

@ -107,6 +107,7 @@
.cp-app-slide-viewer {
width: 50vw;
overflow: hidden;
z-index: 998;
div#cp-app-slide-modal:not(.cp-app-slide-shown) {
position: relative;
top: auto;
@ -417,4 +418,4 @@
pre.cp-slide-css-error {
color: white;
}
}
}

@ -5,7 +5,7 @@
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
html, body {
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