Merge branch 'staging' into foldersColor

pull/1/head
yflory 6 years ago
commit 2c661310c7

@ -1,3 +1,27 @@
# Xenops release (v2.23.0)
## Goals
For this release we wanted to focus on releasing a small set of features built on top of some foundations established in our last release. Since we were able to complete this feature set in less than a week, we decided to bundle them together so users could take benefit from them sooner.
This work is being funded by the grant we received from NLnet foundation as a part of their PET (Privacy Enhancing Technology) fund. You can read all about this grant on our latest blog post (https://blog.cryptpad.fr/2019/05/27/Our-future-is-collaborative/).
## Update notes
* This update only uses clientside dependencies. Fetch the latest code for the core repository, and depending on when you last updated you may need to `bower update` as well.
* User data is "pinned" on CryptPad instances to keep track of what encrypted data can be safely removed. At one point this system was optional and could be disabled by setting `enablePinning = false` in `customize/application_config.js`. At some point we stopped testing whether CryptPad could actually work without pinning enabled, and at this point it is definitely broken. As such, we've decided to drop support for this configuration.
## Features
* Some of our multilingual contributors have contributed translations in the German, Russian, and Italian. The history of their contributions is available on our weblate instance (https://weblate.cryptpad.fr/projects/cryptpad/app/).
* This release introduces a practical use-case of the encrypted mailbox infrastructure which we developed in our last release. Registered users are now able to use this system to accept friend requests and review the status of friend requests that have been accepted or declined. Unlike our previous friend request system, our usage of encrypted mailboxes allows for users to send friend requests from other user's profiles whether or not they are online.
* We've also put some time towards improving user profiles as well. When you change your display name from anywhere within CryptPad the name used in your profile will be updated as well. We've also made updates to other users' profiles render in real-time, since the rest of CryptPad generally updates instantly.
## Bug fixes
* Some small components of CryptPad time out if they don't work within a set amount of time, and apparently this timeout was causing problems in the newest Tor browser version. We've drastically increased the timeout to make it less likely to cause problems when loading very large documents.
* We realized that Weblate was committing "empty strings" to our translation files. Our internationalization system was configured to fall back to the English translation if no translation was available in the user's preferred language, but these empty strings fooled the system into displaying nothing instead. We addressed the issue by checking whether a string was really present, and not just whether a value existed.
# Wolf release (v2.22.0)
## Goals

@ -1,9 +1,26 @@
# 6-stretch is the ONLY node 6 release supported by arm32v7, arm64v8 and x86-64 docker hub labels
FROM node:6-stretch
# We use multi stage builds
FROM node:6-stretch-slim AS build
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq git jq python
RUN npm install -g bower
# install tini in this stage to avoid the need of jq and python
# in the final image
ADD docker-install-tini.sh /usr/local/bin/docker-install-tini.sh
RUN /usr/local/bin/docker-install-tini.sh
COPY . /cryptpad
WORKDIR /cryptpad
RUN npm install --production \
&& npm install -g bower \
&& bower install --allow-root
FROM node:6-stretch-slim
# You want USE_SSL=true if not putting cryptpad behind a proxy
ENV USE_SSL=false
ENV STORAGE=\'./storage/file\'
ENV STORAGE="'./storage/file'"
ENV LOG_TO_STDOUT=true
# Persistent storage needs
@ -17,36 +34,14 @@ VOLUME /cryptpad/block
VOLUME /cryptpad/blob
VOLUME /cryptpad/blobstage
# Required packages
# jq is a build only dependency, removed in cleanup stage
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git jq python
# Install tini for faux init
# sleep 1 is to ensure overlay2 can catch up with the copy prior to running chmod
COPY ./docker-install-tini.sh /
RUN chmod a+x /docker-install-tini.sh \
&& sleep 1 \
&& /docker-install-tini.sh \
&& rm /docker-install-tini.sh
# Cleanup apt
RUN apt-get remove -y --purge jq python \
&& apt-get auto-remove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install cryptpad
COPY . /cryptpad
# Copy cryptpad and tini from the build container
COPY --from=build /sbin/tini /sbin/tini
COPY --from=build /cryptpad /cryptpad
WORKDIR /cryptpad
RUN npm install --production \
&& npm install -g bower \
&& bower install --allow-root
# Unsafe / Safe ports
EXPOSE 3000 3001
# Run cryptpad on startup
CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"]

@ -45,7 +45,7 @@
"localforage": "^1.5.2",
"html2canvas": "^0.4.1",
"croppie": "^2.5.0",
"sortablejs": "#^1.6.0",
"sortablejs": "^1.6.0",
"saferphore": "^0.0.1",
"jszip": "Stuk/jszip#^3.1.5",
"requirejs-plugins": "^1.0.3"

@ -219,6 +219,29 @@ module.exports = {
*/
inactiveTime: 90, // days
/* CryptPad can be configured to remove inactive data which has not been pinned.
* Deletion of data is always risky and as an operator you have the choice to
* archive data instead of deleting it outright. Set this value to true if
* you want your server to archive files and false if you want to keep using
* the old behaviour of simply removing files.
*
* WARNING: this is not implemented universally, so at the moment this will
* only apply to the removal of 'channels' due to inactivity.
*/
retainData: true,
/* As described above, CryptPad offers the ability to archive some data
* instead of deleting it outright. This archived data still takes up space
* and so you'll probably still want to remove these files after a brief period.
* The intent with this feature is to provide a safety net in case of accidental
* deletion. Set this value to the number of days you'd like to retain
* archived data before it's removed permanently.
*
* If 'retainData' is set to false, there will never be any archived data
* to remove.
*/
archiveRetentionTime: 15,
/* Max Upload Size (bytes)
* this sets the maximum size of any one file uploaded to the server.
* anything larger than this size will be rejected
@ -245,12 +268,21 @@ module.exports = {
* ===================== */
/*
CryptPad stores each document in an individual file on your hard drive.
Specify a directory where files should be stored.
It will be created automatically if it does not already exist.
* CryptPad stores each document in an individual file on your hard drive.
* Specify a directory where files should be stored.
* It will be created automatically if it does not already exist.
*/
filePath: './datastore/',
/* CryptPad offers the ability to archive data for a configurable period
* before deleting it, allowing a means of recovering data in the event
* that it was deleted accidentally.
*
* To set the location of this archive directory to a custom value, change
* the path below:
*/
archivePath: './data/archive',
/* CryptPad allows logged in users to request that particular documents be
* stored by the server indefinitely. This is called 'pinning'.
* Pin requests are stored in a pin-store. The location of this store is

@ -0,0 +1,3 @@
The "cptools" font is using the [palette-solid icon](https://fontawesome.com/icons/palette?style=solid) from the "Font Awesome 5 Free" icon set.
This icon is licensed under the [Creative Commons Attribution 4.0 International license](https://fontawesome.com/license/free).

@ -23,4 +23,5 @@
<glyph unicode="&#xe90d;" glyph-name="file" d="M843.294 54.182c0-2.063-1.672-3.735-3.735-3.735v0h-655.119c-2.063 0-3.735 1.672-3.735 3.735v0 787.637c0 2.063 1.672 3.735 3.735 3.735v0h454.054v60.235h-454.054c-35.33 0-63.97-28.64-63.97-63.97v-787.637c0-35.33 28.64-63.97 63.97-63.97v0h655.119c35.33 0 63.97 28.64 63.97 63.97v0 540.19h-60.235zM843.294 696.772l-174.2 209.016v-281.299h234.436l-60.235 72.282z" />
<glyph unicode="&#xe90e;" glyph-name="contacts" d="M875.339 905.788h-655.119c-35.174-0.205-63.608-28.766-63.608-63.969 0 0 0-0.001 0-0.001v0-56.501h28.672c26.614 0 48.188-21.575 48.188-48.188s-21.575-48.188-48.188-48.188v0h-28.672v-192.753h28.672c26.614 0 48.188-21.575 48.188-48.188s-21.575-48.188-48.188-48.188v0h-28.672v-192.753h28.672c26.614 0 48.188-21.575 48.188-48.188s-21.575-48.188-48.188-48.188v0h-28.672v-56.501c0-35.33 28.64-63.97 63.97-63.97v0h654.758c0.107-0.001 0.234-0.001 0.361-0.001 35.33 0 63.97 28.64 63.97 63.97 0 0 0 0.001 0 0.001v0 787.637c0 0 0 0.001 0 0.001 0 35.33-28.64 63.97-63.97 63.97-0.127 0-0.254 0-0.381-0.001h0.020zM730.775 255.247h-368.52c-0.024 0-0.053 0-0.081 0-9.98 0-18.071 8.090-18.071 18.071 0 1.409 0.161 2.781 0.467 4.098l-0.024-0.122c2.111 10.104 6.682 18.914 13.060 26.078l-0.049-0.056c15.661 16.023 78.788 32.045 110.11 48.188 18.94 10.401 31.565 30.213 31.565 52.974 0 0.139 0 0.277-0.001 0.415v-0.021c-1.093 10.266-5.776 19.271-12.751 25.884l-0.019 0.018c-5.568 6.842-10.036 14.847-12.974 23.556l-0.157 0.538c-10.94 4.714-18.46 15.404-18.46 27.85 0 1.053 0.054 2.094 0.159 3.119l-0.011-0.129c-0.097 1.030-0.152 2.228-0.152 3.439 0 7.656 2.211 14.795 6.029 20.814l-0.094-0.159c-1.566 10.722-2.891 22.167-3.735 33.491-0.652 4.066-1.025 8.753-1.025 13.527 0 13.019 2.772 25.391 7.758 36.557l-0.227-0.57c5.785 12.712 18.376 21.391 32.994 21.391 2.219 0 4.391-0.2 6.5-0.583l-0.221 0.033c2.986 9.996 9.527 18.143 18.123 23.149l0.189 0.102c13.975 6.987 112.76 10.481 114.206-85.173 0.143-1.265 0.225-2.731 0.225-4.216s-0.082-2.951-0.241-4.394l0.016 0.178c-0.843-11.324-2.048-22.648-3.614-33.491 3.737-5.787 5.958-12.858 5.958-20.448 0-1.284-0.064-2.553-0.188-3.805l0.013 0.158c0.094-0.897 0.148-1.937 0.148-2.991 0-12.446-7.52-23.136-18.264-27.774l-0.196-0.075c-3.173-9.234-7.632-17.229-13.254-24.252l0.122 0.158c-6.963-6.561-11.64-15.479-12.753-25.474l-0.017-0.187c-0.001-0.153-0.002-0.333-0.002-0.514 0-22.761 12.625-42.574 31.254-52.818l0.312-0.157c31.563-16.023 94.328-32.045 110.231-48.188 6.295-7.112 10.827-15.924 12.828-25.661l0.062-0.361c0.299-1.231 0.47-2.644 0.47-4.096 0-9.934-8.015-17.995-17.932-18.070h-0.007zM184.922 122.729c19.96 0 36.141 16.181 36.141 36.141s-16.181 36.141-36.141 36.141v0h-64.452c-19.96 0-36.141-16.181-36.141-36.141s16.181-36.141 36.141-36.141v0h64.090zM120.471 411.859h64.090c19.96 0 36.141 16.181 36.141 36.141s-16.181 36.141-36.141 36.141v0h-64.090c-19.96 0-36.141-16.181-36.141-36.141s16.181-36.141 36.141-36.141v0zM120.471 700.988h64.090c19.96 0 36.141 16.181 36.141 36.141s-16.181 36.141-36.141 36.141v0h-64.090c-19.96 0-36.141-16.181-36.141-36.141s16.181-36.141 36.141-36.141v0z" />
<glyph unicode="&#xe90f;" glyph-name="code" d="M839.56 905.788h-655.119c-35.33 0-63.97-28.64-63.97-63.97v-787.637c0-35.33 28.64-63.97 63.97-63.97v0h655.119c35.33 0 63.97 28.64 63.97 63.97v0 787.637c0 35.33-28.64 63.97-63.97 63.97v0zM843.294 54.182c0-2.063-1.672-3.735-3.735-3.735v0h-655.119c-2.063 0-3.735 1.672-3.735 3.735v0 787.637c0 2.063 1.672 3.735 3.735 3.735v0h655.119c2.063 0 3.735-1.672 3.735-3.735v0zM445.741 514.259c0 0.036 0 0.078 0 0.121 0 5.928-2.817 11.198-7.185 14.545l-0.044 0.032c-3.983 2.691-8.892 4.295-14.175 4.295-4.094 0-7.962-0.963-11.392-2.675l0.148 0.067-184.2-86.618c-8.222-3.631-13.857-11.713-13.857-21.111 0-0.117 0.001-0.234 0.003-0.351v0.018c-0.012-0.283-0.019-0.616-0.019-0.95 0-9.595 5.609-17.881 13.727-21.756l0.145-0.062 182.874-86.618c3.48-1.929 7.624-3.084 12.033-3.132h0.015c0.048 0 0.104-0.001 0.16-0.001 5.069 0 9.747 1.674 13.511 4.5l-0.058-0.042c4.41 3.332 7.23 8.565 7.23 14.458 0 0.084-0.001 0.168-0.002 0.252v-0.013c-0.071 7.753-4.57 14.439-11.087 17.657l-0.116 0.052-159.021 75.174 159.503 75.174c6.763 2.889 11.483 9.349 11.805 16.947l0.001 0.039zM593.92 607.503c0.002 0.105 0.003 0.229 0.003 0.352 0 5.748-2.249 10.971-5.915 14.836l0.009-0.010c-3.681 4.147-9.026 6.747-14.977 6.747-0.029 0-0.057 0-0.086 0h0.004c-9.851-0.020-18.105-6.831-20.33-16l-0.029-0.143-101.798-317.32c-0.832-2.21-1.355-4.765-1.445-7.429l-0.001-0.040c0-0.049-0.001-0.106-0.001-0.164 0-5.808 2.246-11.091 5.916-15.028l-0.012 0.013c3.716-4.077 9.049-6.626 14.977-6.626 0.029 0 0.058 0 0.086 0h-0.004c4.647 0.168 8.845 1.966 12.066 4.836l-0.019-0.017c3.378 2.964 5.926 6.796 7.301 11.149l0.048 0.175 102.28 317.199c0.984 2.183 1.668 4.715 1.92 7.372l0.007 0.097zM795.106 444.024l-183.236 86.618c-3.456 2.029-7.611 3.228-12.047 3.228-5.294 0-10.189-1.707-14.164-4.601l0.069 0.048c-4.553-3.371-7.472-8.724-7.472-14.758 0-0.106 0.001-0.211 0.003-0.316v0.016c0.315-7.802 5.137-14.404 11.919-17.299l0.128-0.049 158.901-74.812-159.985-75.535c-6.524-3.171-10.945-9.741-10.963-17.345v-0.002c-0.001-0.071-0.002-0.155-0.002-0.24 0-5.892 2.82-11.126 7.184-14.425l0.045-0.033c3.706-2.784 8.384-4.458 13.453-4.458 0.056 0 0.113 0 0.169 0.001h-0.009c0.189-0.005 0.412-0.008 0.636-0.008 4.169 0 8.098 1.028 11.547 2.844l-0.136-0.065 183.959 86.98c8.264 3.938 13.873 12.223 13.873 21.819 0 0.334-0.007 0.667-0.020 0.998l0.002-0.047c0.002 0.099 0.002 0.216 0.002 0.333 0 9.398-5.634 17.48-13.71 21.053l-0.147 0.058z" />
<glyph unicode="&#xe910;" glyph-name="palette" d="M408.6 950c-198.8-38.8-359-198.6-398.2-396.8-74-374 263.4-652.8 517.6-613.4 82.4 12.8 122.8 109.2 85 183.4-46.2 90.8 19.8 196.8 121.8 196.8h159.4c71.6 0 129.6 59.2 129.8 130.6-1 315.2-287.8 563.2-615.4 499.4zM192 320c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM256 576c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM512 704c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM768 576c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

@ -1,9 +1,9 @@
@font-face {
font-family: 'cptools';
src:
url('fonts/cptools.ttf?703wg5') format('truetype'),
url('fonts/cptools.woff?703wg5') format('woff'),
url('fonts/cptools.svg?703wg5#cptools') format('svg');
url('fonts/cptools.ttf?gjk60v') format('truetype'),
url('fonts/cptools.woff?gjk60v') format('woff'),
url('fonts/cptools.svg?gjk60v#cptools') format('svg');
font-weight: normal;
font-style: normal;
}
@ -24,6 +24,9 @@
-moz-osx-font-smoothing: grayscale;
}
.cptools-palette:before {
content: "\e910";
}
.cptools-slide:before {
content: "\e902";
}

@ -1,6 +1,7 @@
(function () {
// add your module to this map so it gets used
var map = {
'ca': 'Català',
'de': 'Deutsch',
'es': 'Español',
'el': 'Ελληνικά',
@ -12,6 +13,7 @@ var map = {
'ro': 'Română',
'ru': 'Русский',
'zh': '繁體中文',
'te': 'తెలుగు',
};
var messages = {};
@ -69,10 +71,25 @@ define(req, function(Util, AppConfig, Default, Language) {
});
}
Util.extend(messages, Default);
var extend = function (a, b) {
for (var k in b) {
if (Util.isObject(b[k])) {
a[k] = Util.isObject(a[k]) ? a[k] : {};
extend(a[k], b[k]);
continue;
}
if (Array.isArray(b[k])) {
a[k] = b[k].slice();
continue;
}
a[k] = b[k] || a[k];
}
};
extend(messages, Default);
if (Language && language !== defaultLanguage) {
// Add the translated keys to the returned object
Util.extend(messages, Language);
extend(messages, Language);
}
messages._languages = map;

@ -144,6 +144,10 @@
padding: @alertify_padding-base;
background: #fff;
box-shadow: @alertify_box-shadow;
&.wide {
width: 1000px;
max-width: 70%;
}
}
.msg {

@ -22,7 +22,6 @@
}
* {
.tools_unselectable();
cursor: default;
}
}

@ -10,7 +10,7 @@
display: flex;
flex-flow: column;
.cp-notification {
height: @notif-height;
min-height: @notif-height;
display: flex;
.cp-notification-content {
flex: 1;
@ -32,8 +32,9 @@
display: none;
align-items: center;
justify-content: center;
span {
cursor: pointer;
&:hover {
background-color: rgba(0,0,0,0.1);
}
}
}

@ -0,0 +1,86 @@
@import (reference) "./colortheme-all.less";
@import (reference) './modal.less';
@import (reference) './alertify.less';
@import (reference) './avatar.less';
@import (reference) './checkmark.less';
@import (reference) './password-input.less';
@import (reference) "./tools.less";
.share_main () {
.alertify_main();
.checkmark_main(20px);
.password_main();
.modal_main();
.cp-share-columns {
display: flex;
flex-flow: row;
.cp-share-column {
width: 50%;
padding: 0 10px;
}
}
.cp-share-grid, .cp-share-list {
.avatar_main(50px);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.cp-share-list {
margin-bottom: 15px;
}
.cp-share-grid {
max-height: 228px;
overflow-x: auto;
}
.cp-recent-only {
.cp-share-grid, .cp-share-grid-filter {
display: none;
}
}
.cp-share-grid-filter {
display: flex;
input {
flex: 1;
min-width: 0;
margin-bottom: 0 !important;
&::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: @colortheme_alertify-primary-text;
opacity: 1; /* Firefox */
}
}
margin-bottom: 15px;
}
.cp-share-friend {
width: 70px;
height: 70px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding: 5px;
margin-bottom: 6px;
cursor: default;
transition: order 0.5s, background-color 0.5s;
.tools_unselectable();
&.cp-selected {
background-color: @colortheme_alertify-primary;
color: @colortheme_alertify-primary-text;
order: -1 !important;
}
.cp-share-friend-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
text-align: center;
}
border: 1px solid @colortheme_alertify-primary;
&.cp-fake-friend {
visibility: hidden;
}
}
}

@ -909,9 +909,6 @@
}
button {
position: relative;
&.fa-bell-o {
cursor: default;
}
.cp-dropdown-button-title {
position: absolute;
bottom: 0;
@ -998,7 +995,6 @@
span {
text-align: center;
width: 100%;
cursor: default;
font-size: 32px;
display: inline-flex;
justify-content: center;

@ -0,0 +1,15 @@
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe, #sbox-share-iframe, #sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
margin:0;
padding:0;
overflow:hidden;
}

@ -17,7 +17,7 @@ Translations can now be made using [Weblate](https://weblate.cryptpad.fr). We ma
## Translate an existing language
* All translations can be done using the Weblate UI. For better help about how to use the tool, please check the [Weblate documentation](https://docs.weblate.org/en/latest/user/index.html).
* All translations can be done using the Weblate UI. For better help about how to use the tool, please check the [Weblate documentation](https://docs.weblate.org/en/latest/).
* Our Weblate instance is configured to always require approval for changes.
### Update an existing translation

@ -0,0 +1,14 @@
/*
* You can override the translation text using this file.
* The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js)
in a 'customize' directory (/customize/translations/messages.{LANG}.js).
* If you want to check all the existing translation keys, you can open the internal language file
but you should not change it directly (/common/translations/messages.{LANG}.js)
*/
define(['/common/translations/messages.ca.js'], function (Messages) {
// Replace the existing keys in your copied file here:
// Messages.button_newpad = "New Rich Text Document";
return Messages;
});

@ -0,0 +1,14 @@
/*
* You can override the translation text using this file.
* The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js)
in a 'customize' directory (/customize/translations/messages.{LANG}.js).
* If you want to check all the existing translation keys, you can open the internal language file
but you should not change it directly (/common/translations/messages.{LANG}.js)
*/
define(['/common/translations/messages.te.js'], function (Messages) {
// Replace the existing keys in your copied file here:
// Messages.button_newpad = "New Rich Text Document";
return Messages;
});

@ -21,7 +21,7 @@ Run from the cryptpad source directory, keeping instance state in `/var/cryptpad
docker build -t xwiki/cryptpad .
docker run --restart=always -d --name cryptpad -p 3000:3000 -p 3001:3001 \
-v /var/cryptpad/files:/cryptpad/datastore \
-v /var/cryptpad/customize:/cryptpad/customize
-v /var/cryptpad/customize:/cryptpad/customize \
-v /var/cryptpad/blob:/cryptpad/blob \
-v /var/cryptpad/blobstage:/cryptpad/blobstage \
-v /var/cryptpad/pins:/cryptpad/pins \

@ -1,11 +1,14 @@
/* jslint node: true */
"use strict";
var config;
var configPath = process.env.CRYPTPAD_CONFIG || "../config/config";
try {
config = require("../config/config");
config = require(configPath);
if (config.adminEmail === 'i.did.not.read.my.config@cryptpad.fr') {
console.log("You can configure the administrator email (adminEmail) in your config/config.js file");
}
} catch (e) {
console.log("You can customize the configuration by copying config/config.example.js to config/config.js");
console.log("Config not found, loading the example config. You can customize the configuration by copying config/config.example.js to " + configPath);
config = require("../config/config.example");
}
module.exports = config;

1304
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -8,7 +8,7 @@
"url": "git://github.com/xwiki-labs/cryptpad.git"
},
"dependencies": {
"chainpad-server": "~3.0.0",
"chainpad-server": "~3.0.2",
"express": "~4.16.0",
"fs-extra": "^7.0.0",
"nthen": "~0.1.0",
@ -18,12 +18,11 @@
"sortify": "^1.0.4",
"stream-to-pull-stream": "^1.7.2",
"tweetnacl": "~0.12.2",
"ws": "^1.0.1",
"ws": "^3.3.1",
"get-folder-size": "^2.0.1"
},
"devDependencies": {
"flow-bin": "^0.59.0",
"heapdump": "^0.3.9",
"jshint": "~2.9.1",
"less": "2.7.1",
"lesshint": "^4.5.0",

@ -62,13 +62,13 @@ via our [GitHub issue tracker](https://github.com/xwiki-labs/cryptpad/issues/),
[Matrix channel](https://riot.im/app/#/room/#cryptpad:matrix.org), or by
[e-mail](mailto:research@xwiki.com).
# Team
CryptPad is actively developed by a team at [XWiki SAS](https://www.xwiki.com), a company that has been building Open-Source software since 2004 with contributors from around the world. Between 2015 and 2019 it was funded by a research grant from the French state through [BPI France](https://www.bpifrance.fr/). It is currently financed by [NLnet PET](https://nlnet.nl/PET/), subscribers of CryptPad.fr and donations to our [Open-Collective campaign](https://opencollective.com/cryptpad).
# Contributing
We love Open Source and we love contribution. It is our intent to keep this project available
under the AGPL license forever but in order to finance more development on this and other FOSS
projects, we also wish to sell other licenses to this software. Before making a pull request,
please read and
[sign the Commons Management Agreement](https://www.clahub.com/agreements/cjdelisle/cryptpad).
We love Open Source and we love contribution. Learn more about [contributing](https://github.com/xwiki-labs/cryptpad/wiki/Contributor-overview).
If you have any questions or comments, or if you're interested in contributing to Cryptpad, come say hi on IRC, `#cryptpad` on Freenode.

@ -814,6 +814,7 @@ var clearOwnedChannel = function (Env, channelId, unsafeKey, cb) {
return void cb('INSUFFICIENT_PERMISSIONS');
}
// FIXME COLDSTORAGE
return void Env.msgStore.clearChannel(channelId, function (e) {
cb(e);
});
@ -900,6 +901,20 @@ var removeOwnedChannel = function (Env, channelId, unsafeKey, cb) {
if (metadata.owners.indexOf(unsafeKey) === -1) {
return void cb('INSUFFICIENT_PERMISSIONS');
}
// if the admin has configured data retention...
// temporarily archive the file instead of removing it
if (Env.retainData) {
return void Env.msgStore.archiveChannel(channelId, function (e) {
Log.info('ARCHIVAL_CHANNEL_BY_OWNER_RPC', {
unsafeKey: unsafeKey,
channelId: channelId,
status: e? String(e): 'SUCCESS',
});
cb(e);
});
}
return void Env.msgStore.removeChannel(channelId, function (e) {
Log.info('DELETION_CHANNEL_BY_OWNER_RPC', {
unsafeKey: unsafeKey,
@ -1430,6 +1445,7 @@ var removeLoginBlock = function (Env, msg, cb) {
return void cb('E_INVALID_BLOCK_PATH');
}
// FIXME COLDSTORAGE
Fs.unlink(path, function (err) {
Log.info('DELETION_BLOCK_BY_OWNER_RPC', {
publicKey: publicKey,
@ -1645,6 +1661,7 @@ RPC.create = function (
};
var Env = {
retainData: config.retainData || false,
defaultStorageLimit: config.defaultStorageLimit,
maxUploadSize: config.maxUploadSize || (20 * 1024 * 1024),
Sessions: {},

@ -7,6 +7,14 @@ const config = require("../lib/load-config");
if (!config.inactiveTime || typeof(config.inactiveTime) !== "number") { return; }
/* Instead of this script you should probably use
evict-inactive.js which moves things to an archive directory
in case the data that would have been deleted turns out to be important.
it also handles removing that archived data after a set period of time
it only works for channels at the moment, though, and nothing else.
*/
let inactiveTime = +new Date() - (config.inactiveTime * 24 * 3600 * 1000);
let inactiveConfig = {
unpinned: true,

@ -0,0 +1,63 @@
var nThen = require("nthen");
var Store = require("../storage/file");
var config = require("../lib/load-config");
var store;
var Log;
nThen(function (w) {
// load the store which will be used for iterating over channels
// and performing operations like archival and deletion
Store.create(config, w(function (_) {
store = _;
}));
// load the logging module so that you have a record of which
// files were archived or deleted at what time
var Logger = require("../lib/log");
Logger.create(config, w(function (_) {
Log = _;
}));
}).nThen(function (w) {
// count the number of files which have been restored in this run
var conflicts = 0;
var handler = function (err, item, cb) {
if (err) {
Log.error('DIAGNOSE_ARCHIVE_CONFLICTS_ITERATION', err);
return void cb();
}
// check if such a file exists on the server
store.isChannelAvailable(item.channel, function (err, available) {
// weird edge case?
if (err) { return void cb(); }
// the channel doesn't exist in the database
if (!available) { return void cb(); }
// the channel is available
// that means it's a duplicate of something in the archive
conflicts++;
Log.info('DIAGNOSE_ARCHIVE_CONFLICT_DETECTED', item.channel);
cb();
});
};
// if you hit an error, log it
// otherwise, when there are no more channels to process
// log some stats about how many were removed
var done = function (err) {
if (err) {
return Log.error('DIAGNOSE_ARCHIVE_CONFLICTS_FINAL_ERROR', err);
}
Log.info('DIAGNOSE_ARCHIVE_CONFLICTS_COUNT', conflicts);
};
store.listArchivedChannels(handler, w(done));
}).nThen(function () {
// the store will keep this script running if you don't shut it down
store.shutdown();
Log.shutdown();
});

@ -0,0 +1,169 @@
var nThen = require("nthen");
var Store = require("../storage/file");
var Pinned = require("./pinned");
var config = require("../lib/load-config");
// the administrator should have set an 'inactiveTime' in their config
// if they didn't, just exit.
if (!config.inactiveTime || typeof(config.inactiveTime) !== "number") { return; }
// files which have not been changed since before this date can be considered inactive
var inactiveTime = +new Date() - (config.inactiveTime * 24 * 3600 * 1000);
// files which were archived before this date can be considered safe to remove
var retentionTime = +new Date() - (config.archiveRetentionTime * 24 * 3600 * 1000);
var store;
var pins;
var Log;
nThen(function (w) {
// load the store which will be used for iterating over channels
// and performing operations like archival and deletion
Store.create(config, w(function (_) {
store = _;
})); // load the list of pinned files so you know which files
// should not be archived or deleted
Pinned.load(w(function (err, _) {
if (err) {
w.abort();
return void console.error(err);
}
pins = _;
}), {
pinPath: config.pinPath,
});
// load the logging module so that you have a record of which
// files were archived or deleted at what time
var Logger = require("../lib/log");
Logger.create(config, w(function (_) {
Log = _;
}));
}).nThen(function (w) {
// this block will iterate over archived channels and remove them
// if they've been in cold storage for longer than your configured archive time
// if the admin has not set an 'archiveRetentionTime', this block makes no sense
// so just skip it
if (typeof(config.archiveRetentionTime) !== "number") { return; }
// count the number of files which have been removed in this run
var removed = 0;
var handler = function (err, item, cb) {
if (err) {
Log.error('EVICT_ARCHIVED_CHANNEL_ITERATION', err);
return void cb();
}
// don't mess with files that are freshly stored in cold storage
// based on ctime because that's changed when the file is moved...
if (+new Date(item.ctime) > retentionTime) {
return void cb();
}
// but if it's been stored for the configured time...
// expire it
store.removeArchivedChannel(item.channel, w(function (err) {
if (err) {
Log.error('EVICT_ARCHIVED_CHANNEL_REMOVAL_ERROR', {
error: err,
channel: item.channel,
});
return void cb();
}
Log.info('EVICT_ARCHIVED_CHANNEL_REMOVAL', item.channel);
removed++;
cb();
}));
};
// if you hit an error, log it
// otherwise, when there are no more channels to process
// log some stats about how many were removed
var done = function (err) {
if (err) {
return Log.error('EVICT_ARCHIVED_FINAL_ERROR', err);
}
Log.info('EVICT_ARCHIVED_CHANNELS_REMOVED', removed);
};
store.listArchivedChannels(handler, w(done));
}).nThen(function (w) {
var removed = 0;
var channels = 0;
var archived = 0;
var handler = function (err, item, cb) {
channels++;
if (err) {
Log.error('EVICT_CHANNEL_ITERATION', err);
return void cb();
}
// check if the database has any ephemeral channels
// if it does it's because of a bug, and they should be removed
if (item.channel.length === 34) {
return void store.removeChannel(item.channel, w(function (err) {
if (err) {
Log.error('EVICT_EPHEMERAL_CHANNEL_REMOVAL_ERROR', {
error: err,
channel: item.channel,
});
return void cb();
}
Log.info('EVICT_EPHEMERAL_CHANNEL_REMOVAL', item.channel);
cb();
}));
}
// bail out if the channel was modified recently
if (+new Date(item.mtime) > inactiveTime) { return void cb(); }
// ignore the channel if it's pinned
if (pins[item.channel]) { return void cb(); }
// if the server is configured to retain data, archive the channel
if (config.retainData) {
return void store.archiveChannel(item.channel, w(function (err) {
if (err) {
Log.error('EVICT_CHANNEL_ARCHIVAL_ERROR', {
error: err,
channel: item.channel,
});
return void cb();
}
Log.info('EVICT_CHANNEL_ARCHIVAL', item.channel);
archived++;
cb();
}));
}
// otherwise remove it
store.removeChannel(item.channel, w(function (err) {
if (err) {
Log.error('EVICT_CHANNEL_REMOVAL_ERROR', {
error: err,
channel: item.channel,
});
return void cb();
}
Log.info('EVICT_CHANNEL_REMOVAL', item.channel);
removed++;
cb();
}));
};
var done = function () {
if (config.retainData) {
return void Log.info('EVICT_CHANNELS_ARCHIVED', archived);
}
return void Log.info('EVICT_CHANNELS_REMOVED', removed);
};
store.listChannels(handler, w(done));
}).nThen(function () {
// the store will keep this script running if you don't shut it down
store.shutdown();
Log.shutdown();
});

@ -0,0 +1,61 @@
var nThen = require("nthen");
var Store = require("../storage/file");
var config = require("../lib/load-config");
var store;
var Log;
nThen(function (w) {
// load the store which will be used for iterating over channels
// and performing operations like archival and deletion
Store.create(config, w(function (_) {
store = _;
}));
// load the logging module so that you have a record of which
// files were archived or deleted at what time
var Logger = require("../lib/log");
Logger.create(config, w(function (_) {
Log = _;
}));
}).nThen(function (w) {
// count the number of files which have been restored in this run
var restored = 0;
var handler = function (err, item, cb) {
if (err) {
Log.error('RESTORE_ARCHIVED_CHANNEL_ITERATION', err);
return void cb();
}
store.restoreArchivedChannel(item.channel, w(function (err) {
if (err) {
Log.error('RESTORE_ARCHIVED_CHANNEL_RESTORATION_ERROR', {
error: err,
channel: item.channel,
});
return void cb();
}
Log.info('RESTORE_ARCHIVED_CHANNEL_RESTORATION', item.channel);
restored++;
cb();
}));
};
// if you hit an error, log it
// otherwise, when there are no more channels to process
// log some stats about how many were removed
var done = function (err) {
if (err) {
return Log.error('RESTORE_ARCHIVED_FINAL_ERROR', err);
}
Log.info('RESTORE_ARCHIVED_CHANNELS_RESTORED', restored);
};
store.listArchivedChannels(handler, w(done));
}).nThen(function () {
// the store will keep this script running if you don't shut it down
store.shutdown();
Log.shutdown();
});

@ -5,6 +5,7 @@ var Fs = require("fs");
var Fse = require("fs-extra");
var Path = require("path");
var nThen = require("nthen");
var Semaphore = require("saferphore");
const ToPull = require('stream-to-pull-stream');
const Pull = require('pull-stream');
@ -14,10 +15,18 @@ const isValidChannelId = function (id) {
/^[a-zA-Z0-9=+-]*$/.test(id);
};
// 511 -> octal 777
// read, write, execute permissions flag
const PERMISSIVE = 511;
var mkPath = function (env, channelId) {
return Path.join(env.root, channelId.slice(0, 2), channelId) + '.ndjson';
};
var mkArchivePath = function (env, channelId) {
return Path.join(env.archiveRoot, 'datastore', channelId.slice(0, 2), channelId) + '.ndjson';
};
var getMetadataAtPath = function (Env, path, cb) {
var remainder = '';
var stream = Fs.createReadStream(path, { encoding: 'utf8' });
@ -68,7 +77,7 @@ var closeChannel = function (env, channelName, cb) {
}
};
var clearChannel = function (env, channelId, cb) { // FIXME deletion
var clearChannel = function (env, channelId, cb) {
var path = mkPath(env, channelId);
getMetadataAtPath(env, path, function (e, metadata) {
if (e) { return cb(new Error(e)); }
@ -189,8 +198,7 @@ var checkPath = function (path, callback) {
callback(err);
return;
}
// 511 -> octal 777
Fse.mkdirp(Path.dirname(path), 511, function (err) {
Fse.mkdirp(Path.dirname(path), PERMISSIVE, function (err) {
if (err && err.code !== 'EEXIST') {
callback(err);
return;
@ -200,11 +208,128 @@ var checkPath = function (path, callback) {
});
};
var removeChannel = function (env, channelName, cb) { // FIXME deletion
var removeChannel = function (env, channelName, cb) {
var filename = mkPath(env, channelName);
Fs.unlink(filename, cb);
};
// pass in the path so we can reuse the same function for archived files
var channelExists = function (filepath, channelName, cb) {
Fs.stat(filepath, function (err, stat) {
if (err) {
if (err.code === 'ENOENT') {
// no, the file doesn't exist
return void cb(void 0, false);
}
return void cb(err);
}
if (!stat.isFile()) { return void cb("E_NOT_FILE"); }
return void cb(void 0, true);
});
};
var removeArchivedChannel = function (env, channelName, cb) {
var filename = mkArchivePath(env, channelName);
Fs.unlink(filename, cb);
};
var listChannels = function (root, handler, cb) {
// do twenty things at a time
var sema = Semaphore.create(20);
var dirList = [];
nThen(function (w) {
// the root of your datastore contains nested directories...
Fs.readdir(root, w(function (err, list) {
if (err) {
w.abort();
// TODO check if we normally return strings or errors
return void cb(err);
}
dirList = list;
}));
}).nThen(function (w) {
// search inside the nested directories
// stream it so you don't put unnecessary data in memory
var wait = w();
dirList.forEach(function (dir) {
sema.take(function (give) {
var nestedDirPath = Path.join(root, dir);
Fs.readdir(nestedDirPath, w(give(function (err, list) {
if (err) { return void handler(err); } // Is this correct?
list.forEach(function (item) {
// ignore things that don't match the naming pattern
if (/^\./.test(item) || !/[0-9a-fA-F]{32,}\.ndjson$/.test(item)) { return; }
var filepath = Path.join(nestedDirPath, item);
var channel = filepath.replace(/\.ndjson$/, '').replace(/.*\//, '');
if ([32, 34].indexOf(channel.length) === -1) { return; }
// otherwise throw it on the pile
sema.take(function (give) {
var next = w(give());
Fs.stat(filepath, w(function (err, stats) {
if (err) {
return void handler(err);
}
handler(void 0, {
channel: channel,
atime: stats.atime,
mtime: stats.mtime,
ctime: stats.ctime,
size: stats.size,
}, next);
}));
});
});
})));
});
});
wait();
}).nThen(function () {
cb();
});
};
// move a channel's log file from its current location
// to an equivalent location in the cold storage directory
var archiveChannel = function (env, channelName, cb) {
if (!env.retainData) {
return void cb("ARCHIVES_DISABLED");
}
// ctime is the most reliable indicator of when a file was archived
// because it is used to indicate changes to the files metadata
// and not its contents
// if we find that this is not reliable in production, we can update it manually
// https://nodejs.org/api/fs.html#fs_fs_utimes_path_atime_mtime_callback
// check what the channel's path should be (in its current location)
var currentPath = mkPath(env, channelName);
// construct a parallel path in the new location
var archivePath = mkArchivePath(env, channelName);
// use Fse.move to move it, Fse makes paths to the directory when you use it.
// https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/move.md
Fse.move(currentPath, archivePath, { overwrite: true }, cb);
};
var unarchiveChannel = function (env, channelName, cb) {
// very much like 'archiveChannel' but in the opposite direction
// the file is currently archived
var currentPath = mkArchivePath(env, channelName);
var unarchivedPath = mkPath(env, channelName);
// if a file exists in the unarchived path, you probably don't want to clobber its data
// so unlike 'archiveChannel' we won't overwrite.
// Fse.move will call back with EEXIST in such a situation
Fse.move(currentPath, unarchivedPath, cb);
};
var flushUnusedChannels = function (env, cb, frame) {
var currentTime = +new Date();
@ -413,19 +538,30 @@ module.exports.create = function (
) {
var env = {
root: conf.filePath || './datastore',
archiveRoot: conf.archivePath || './data/archive',
retainData: conf.retainData,
channels: { },
channelExpirationMs: conf.channelExpirationMs || 30000,
verbose: conf.verbose,
openFiles: 0,
openFileLimit: conf.openFileLimit || 2048,
};
// 0x1ff -> 777
var it;
Fse.mkdirp(env.root, 0x1ff, function (err) {
nThen(function (w) {
// make sure the store's directory exists
Fse.mkdirp(env.root, PERMISSIVE, w(function (err) {
if (err && err.code !== 'EEXIST') {
// TODO: somehow return a nice error
throw err;
}
}));
// make sure the cold storage directory exists
Fse.mkdirp(env.archiveRoot, PERMISSIVE, w(function (err) {
if (err && err.code !== 'EEXIST') {
throw err;
}
}));
}).nThen(function () {
cb({
readMessagesBin: (channelName, start, asyncMsgHandler, cb) => {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
@ -449,6 +585,10 @@ module.exports.create = function (
cb(err);
});
},
removeArchivedChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
removeArchivedChannel(env, channelName, cb);
},
closeChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
closeChannel(env, channelName, cb);
@ -468,6 +608,32 @@ module.exports.create = function (
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
clearChannel(env, channelName, cb);
},
listChannels: function (handler, cb) {
listChannels(env.root, handler, cb);
},
isChannelAvailable: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
// construct the path
var filepath = mkPath(env, channelName);
channelExists(filepath, channelName, cb);
},
isChannelArchived: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
// construct the path
var filepath = mkArchivePath(env, channelName);
channelExists(filepath, channelName, cb);
},
listArchivedChannels: function (handler, cb) {
listChannels(Path.join(env.archiveRoot, 'datastore'), handler, cb);
},
archiveChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
archiveChannel(env, channelName, cb);
},
restoreArchivedChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
unarchiveChannel(env, channelName, cb);
},
log: function (channelName, content, cb) {
message(env, channelName, content, cb);
},

@ -83,6 +83,7 @@ var write = function (env, task, cb) {
};
var remove = function (env, path, cb) {
// FIXME COLDSTORAGE?
Fs.unlink(path, cb);
};

@ -6,33 +6,7 @@
<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>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -203,7 +203,7 @@ define([
};
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle'];
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
var configTb = {
displayed: displayed,
sfCommon: common,

@ -6,33 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<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;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -156,12 +156,14 @@ define([
]);
};
dialog.frame = function (content) {
dialog.frame = function (content, opt) {
opt = opt || {};
var cls = opt.wide ? '.wide' : '';
return $(h('div.alertify', {
tabindex: 1,
}, [
h('div.dialog', [
h('div', content),
h('div'+cls, content),
])
])).click(function (e) {
e.stopPropagation();
@ -351,6 +353,9 @@ define([
var close = function (el) {
var $el = $(el).fadeOut(150, function () {
$el.detach();
if (opt.onClose) {
opt.onClose();
}
});
};
@ -373,10 +378,10 @@ define([
if (opt.forefront) { $(frame).addClass('forefront'); }
return frame;
};
UI.openCustomModal = function (content) {
UI.openCustomModal = function (content, opt) {
var frame = dialog.frame([
content
]);
], opt);
$(frame).find('button[data-keys]').each(function (i, el) {
var keys = JSON.parse($(el).attr('data-keys'));
customListenForKeys(keys, function () {
@ -838,41 +843,23 @@ define([
// This is the robust solution to remove dangling tooltips
// The mutation observer does not always find removed nodes.
//setInterval(UI.clearTooltips, delay);
var checkRemoved = function (x) {
var out = false;
var xId = $(x).attr('aria-describedby');
if (xId) {
if (xId.indexOf('tippy-') === 0) {
return true;
}
}
$(x).find('[aria-describedby]').each(function (i, el) {
var id = el.getAttribute('aria-describedby');
if (id.indexOf('tippy-') !== 0) { return; }
out = true;
});
return out;
};
$('[title]').each(addTippy);
var observer = new MutationObserver(function(mutations) {
var removed = false;
mutations.forEach(function(mutation) {
if (mutation.type === "childList") {
for (var i = 0; i < mutation.addedNodes.length; i++) {
if ($(mutation.addedNodes[i]).attr('title')) {
addTippy(0, mutation.addedNodes[i]);
}
$(mutation.addedNodes[i]).find('[title]').each(addTippy);
}
for (var j = 0; j < mutation.removedNodes.length; j++) {
removed |= checkRemoved(mutation.removedNodes[j]);
if (mutation.removedNodes.length !== 0) {
UI.clearTooltips();
}
}
if (mutation.type === "attributes" && mutation.attributeName === "title") {
addTippy(0, mutation.target);
}
});
if (removed) { UI.clearTooltips(); }
});
observer.observe($('body')[0], {
attributes: true,

@ -78,5 +78,27 @@ define([
});
};
Msg.updateMyData = function (store, curve) {
if (store.messenger) {
store.messenger.updateMyData();
}
var myData = createData(store.proxy);
var todo = function (friend) {
if (!friend || !friend.notifications) { return; }
myData.channel = friend.channel;
store.mailbox.sendTo('UPDATE_DATA', myData, {
channel: friend.notifications,
curvePublic: friend.curvePublic
}, function (obj) {
if (obj && obj.error) { console.error(obj); }
});
};
if (curve) {
var friend = getFriend(store.proxy, curve);
return void todo(friend);
}
eachFriend(store.proxy.friends || {}, todo);
};
return Msg;
});

@ -44,6 +44,7 @@ define([
profile: proxy.profile && proxy.profile.view,
edPublic: proxy.edPublic,
curvePublic: proxy.curvePublic,
notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']),
avatar: proxy.profile && proxy.profile.avatar
};
};
@ -519,6 +520,7 @@ define([
// History cleared while we're in the channel
if (parsed.error === 'ECLEARED') {
setChannelHead(parsed.channel, '', function () {});
channels[parsed.channel].messages = [];
emit('CLEAR_CHANNEL', parsed.channel);
return;
}
@ -611,6 +613,15 @@ define([
cb();
});
});
} else {
removeFromFriendList(curvePublic, function () {
delete channels[channel.id];
emit('UNFRIEND', {
curvePublic: curvePublic,
fromMe: true
});
cb();
});
}
channel.wc.bcast(cryptMsg).then(function () {}, function (err) {
console.error(err);
@ -943,6 +954,16 @@ define([
cb();
};
var clearOwnedChannel = function (id, cb) {
var channel = getChannel(id);
if (!channel) { return void cb({error: 'NO_CHANNEL'}); }
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
store.rpc.clearOwnedChannel(id, function (err) {
cb({error:err});
});
channel.messages = [];
};
network.on('disconnect', function () {
emit('DISCONNECT');
});
@ -1000,6 +1021,9 @@ define([
if (cmd === 'SET_CHANNEL_HEAD') {
return void setChannelHead(data.id, data.sig, cb);
}
if (cmd === 'CLEAR_OWNED_CHANNEL') {
return void clearOwnedChannel(data, cb);
}
};
Object.freeze(messenger);

@ -205,7 +205,7 @@ define([
if (content === oldThumbnailState) { return; }
oldThumbnailState = content;
Thumb.fromDOM(opts, function (err, b64) {
Thumb.setPadThumbnail(common, opts.href, null, b64);
Thumb.setPadThumbnail(common, opts.type, null, b64);
});
};
var nafa = Util.notAgainForAnother(mkThumbnail, Thumb.UPDATE_INTERVAL);
@ -243,11 +243,10 @@ define([
var getKey = function (type, channel) {
return 'thumbnail-' + type + '-' + channel;
};
Thumb.setPadThumbnail = function (common, href, channel, b64, cb) {
Thumb.setPadThumbnail = function (common, type, channel, b64, cb) {
cb = cb || function () {};
var parsed = Hash.parsePadUrl(href);
channel = channel || common.getMetadataMgr().getPrivateData().channel;
var k = getKey(parsed.type, channel);
var k = getKey(type, channel);
common.setThumbnail(k, b64, cb);
};
Thumb.displayThumbnail = function (common, href, channel, password, $container, cb) {
@ -270,7 +269,7 @@ define([
if (!v) {
v = 'EMPTY';
}
Thumb.setPadThumbnail(common, href, hexFileName, v, function (err) {
Thumb.setPadThumbnail(common, parsed.type, hexFileName, v, function (err) {
if (!metadata.thumbnail) { return; }
addThumbnail(err, metadata.thumbnail, $container, cb);
});

@ -288,6 +288,15 @@ define([
$d.append(UI.dialog.selectable(formatted, {
id: 'cp-app-prop-size',
}));
if (data.sharedFolder) { // XXX debug
$('<label>', {'for': 'cp-app-prop-channel'}).text('Channel ID').appendTo($d);
if (AppConfig.pinBugRecovery) { $d.append(h('p', AppConfig.pinBugRecovery)); }
$d.append(UI.dialog.selectable(data.channel, {
id: 'cp-app-prop-link',
}));
}
cb(void 0, $d);
});
} else {
@ -316,39 +325,212 @@ define([
});
};
var getFriendsList = function (config) {
var common = config.common;
var title = config.title;
var friends = config.friends;
var myName = common.getMetadataMgr().getUserData().name;
var order = [];
if (!friends) { return; }
var others = Object.keys(friends).map(function (curve, i) {
if (curve.length <= 40) { return; }
var data = friends[curve];
if (!data.notifications) { return; }
var avatar = h('span.cp-share-friend-avatar.cp-avatar');
UIElements.displayAvatar(common, $(avatar), data.avatar, data.displayName);
return h('div.cp-share-friend', {
'data-curve': data.curvePublic,
'data-name': data.displayName,
'data-order': i,
title: data.displayName,
style: 'order:'+i+';'
},[
avatar,
h('span.cp-share-friend-name', data.displayName)
]);
}).filter(function (x) { return x; });
var smallCurves = Object.keys(friends).map(function (c) {
return friends[c].curvePublic.slice(0,8);
});
var noOthers = others.length === 0 ? '.cp-recent-only' : '';
var buttonSelect = h('button.cp-share-with-friends', Messages.share_selectAll);
var buttonDeselect = h('button.cp-share-with-friends', Messages.share_deselectAll);
var inputFilter = h('input', {
placeholder: Messages.share_filterFriend
});
var div = h('div.cp-share-friends.cp-share-column' + noOthers, [
h('label', Messages.share_linkFriends),
h('div.cp-share-grid-filter', [
inputFilter,
buttonSelect,
buttonDeselect
]),
]);
var $div = $(div);
// Fill with fake friends to have a uniform spacing (from the flexbox)
var addFake = function (els) {
$div.find('.cp-fake-friend').remove();
var n = (6 - els.length%6)%6;
for (var j = 0; j < n; j++) {
els.push(h('div.cp-share-friend.cp-fake-friend', {
style: 'order:9999999;'
}));
}
};
addFake(others);
// Hide friends when they are filtered using the text input
var redraw = function () {
var name = $(inputFilter).val().trim().replace(/"/g, '');
$div.find('.cp-share-friend').show();
if (!name) { return; }
$div.find('.cp-share-friend:not(.cp-selected):not([data-name*="'+name+'"])').hide();
};
$(inputFilter).on('keydown keyup change', redraw);
// Replace "copy link" by "share with friends" if at least one friedn is selected
// Also create the "share with friends" button if it doesn't exist
var refreshButtons = function () {
var $nav = $div.parents('.alertify').find('nav');
if (!$nav.find('.cp-share-with-friends').length) {
var button = h('button.primary.cp-share-with-friends', {
'data-keys': '[13]'
}, Messages.share_withFriends);
$(button).click(function () {
var href = Hash.getRelativeHref($('#cp-share-link-preview').val());
var $friends = $div.find('.cp-share-friend.cp-selected');
$friends.each(function (i, el) {
var curve = $(el).attr('data-curve');
if (!curve || !friends[curve]) { return; }
var friend = friends[curve];
if (!friend.notifications || !friend.curvePublic) { return; }
common.mailbox.sendTo("SHARE_PAD", {
href: href,
name: myName,
title: title
}, {
channel: friend.notifications,
curvePublic: friend.curvePublic
});
});
UI.findCancelButton().click();
// Update the "recently shared with" array:
// Get the selected curves
var curves = $friends.toArray().map(function (el) {
return ($(el).attr('data-curve') || '').slice(0,8);
}).filter(function (x) { return x; });
// Prepend them to the "order" array
Array.prototype.unshift.apply(order, curves);
order = Util.deduplicateString(order);
// Make sure we don't have "old" friends and save
order = order.filter(function (curve) {
return smallCurves.indexOf(curve) !== -1;
});
common.setAttribute(['general', 'share-friends'], order);
});
$nav.append(button);
}
var friendMode = $div.find('.cp-share-friend.cp-selected').length;
if (friendMode) {
$nav.find('button.primary[data-keys]').hide();
$nav.find('button.cp-share-with-friends').show();
} else {
$nav.find('button.primary[data-keys]').show();
$nav.find('button.cp-share-with-friends').hide();
}
};
$(buttonSelect).click(function () {
$div.find('.cp-share-friend:not(.cp-fake-friend):not(.cp-selected):visible').addClass('cp-selected');
refreshButtons();
});
$(buttonDeselect).click(function () {
$div.find('.cp-share-friend.cp-selected').removeClass('cp-selected').each(function (i, el) {
var order = $(el).attr('data-order');
if (!order) { return; }
$(el).attr('style', 'order:'+order);
});
redraw();
refreshButtons();
});
common.getAttribute(['general', 'share-friends'], function (err, val) {
order = val || [];
// Sort friends by "recently shared with"
others.sort(function (a, b) {
var ca = ($(a).attr('data-curve') || '').slice(0,8);
var cb = ($(b).attr('data-curve') || '').slice(0,8);
if (!ca && !cb) { return 0; }
if (!ca) { return 1; }
if (!cb) { return -1; }
var ia = order.indexOf(ca);
var ib = order.indexOf(cb);
if (ia === -1 && ib === -1) { return 0; }
if (ia === -1) { return 1; }
if (ib === -1) { return -1; }
return ia - ib;
});
// Reorder the friend icons
others.forEach(function (el, i) {
if ($(el).is('.cp-fake-friend')) { return; }
$(el).attr('data-order', i).css('order', i);
});
// Display them
$div.append(h('div.cp-share-grid', others));
$div.find('.cp-share-friend').click(function () {
var sel = $(this).hasClass('cp-selected');
if (!sel) {
$(this).addClass('cp-selected');
} else {
var order = $(this).attr('data-order');
order = order ? 'order:'+order : '';
$(this).removeClass('cp-selected').attr('style', order);
}
refreshButtons();
});
});
return div;
};
UIElements.createShareModal = function (config) {
var origin = config.origin;
var pathname = config.pathname;
var hashes = config.hashes;
var common = config.common;
if (!hashes) { return; }
// Share link tab
var link = h('div.cp-share-modal', [
var hasFriends = Object.keys(config.friends || {}).length !== 0;
var friendsList = hasFriends ? getFriendsList(config) : undefined;
var friendsUIClass = hasFriends ? '.cp-share-columns' : '';
var link = h('div.cp-share-modal' + friendsUIClass, [
h('div.cp-share-column', [
hasFriends ? h('p', Messages.share_description) : undefined,
h('label', Messages.share_linkAccess),
h('br'),
UI.createRadio('cp-share-editable', 'cp-share-editable-true',
Messages.share_linkEdit, true, { mark: {tabindex:1} }),
UI.createRadio('cp-share-editable', 'cp-share-editable-false',
Messages.share_linkView, false, { mark: {tabindex:1} }),
/*h('input#cp-share-editable-true.cp-share-editable-value', {
type: 'radio',
name: 'cp-share-editable',
value: 1,
}),
h('label', { 'for': 'cp-share-editable-true' }, Messages.share_linkEdit),
h('input#cp-share-editable-false.cp-share-editable-value', {
type: 'radio',
name: 'cp-share-editable',
value: 0
}),
h('label', { 'for': 'cp-share-editable-false' }, Messages.share_linkView),*/
h('br'),
h('label', Messages.share_linkOptions),
h('br'),
UI.createCheckbox('cp-share-embed', Messages.share_linkEmbed, false, { mark: {tabindex:1} }),
UI.createCheckbox('cp-share-present', Messages.share_linkPresent, false, { mark: {tabindex:1} }),
h('br'),
UI.dialog.selectable('', { id: 'cp-share-link-preview', tabindex: 1 })
UI.dialog.selectable('', { id: 'cp-share-link-preview', tabindex: 1 }),
]),
friendsList
]);
if (!hashes.editHash) {
$(link).find('#cp-share-editable-false').attr('checked', true);
@ -380,6 +562,7 @@ define([
$(link).find('#cp-share-link-preview').val(getLinkValue());
});
var linkButtons = [{
className: 'cancel',
name: Messages.cancel,
onClick: function () {},
keys: [27]
@ -403,7 +586,10 @@ define([
},
keys: [[13, 'ctrl']]
}];
var frameLink = UI.dialog.customModal(link, {buttons: linkButtons});
var frameLink = UI.dialog.customModal(link, {
buttons: linkButtons,
onClose: config.onClose,
});
// Embed tab
var getEmbedValue = function () {
@ -420,6 +606,7 @@ define([
UI.dialog.selectable(getEmbedValue())
]);
var embedButtons = [{
className: 'cancel',
name: Messages.cancel,
onClick: function () {},
keys: [27]
@ -433,7 +620,10 @@ define([
},
keys: [13]
}];
var frameEmbed = UI.dialog.customModal(embed, { buttons: embedButtons});
var frameEmbed = UI.dialog.customModal(embed, {
buttons: embedButtons,
onClose: config.onClose,
});
// Create modal
var tabs = [{
@ -464,7 +654,9 @@ define([
$(link).find('#cp-share-link-preview').val(getLinkValue(val));
});
common.getMetadataMgr().onChange(function () {
hashes = common.getMetadataMgr().getPrivateData().availableHashes;
// "hashes" is only available is the secure "share" app
hashes = common.getMetadataMgr().getPrivateData().hashes;
if (!hashes) { return; }
$(link).find('#cp-share-link-preview').val(getLinkValue());
});
return tabs;
@ -481,12 +673,20 @@ define([
// Share link tab
var link = h('div.cp-share-modal', [
UI.dialog.selectable('', { id: 'cp-share-link-preview' })
var hasFriends = Object.keys(config.friends || {}).length !== 0;
var friendsList = hasFriends ? getFriendsList(config) : undefined;
var friendsUIClass = hasFriends ? '.cp-share-columns' : '';
var link = h('div.cp-share-modal' + friendsUIClass, [
h('div.cp-share-column', [
hasFriends ? h('p', Messages.share_description) : undefined,
UI.dialog.selectable('', { id: 'cp-share-link-preview' }),
]),
friendsList
]);
var getLinkValue = function () { return url; };
$(link).find('#cp-share-link-preview').val(getLinkValue());
var linkButtons = [{
className: 'cancel',
name: Messages.cancel,
onClick: function () {},
keys: [27]
@ -513,6 +713,7 @@ define([
UI.dialog.selectable(common.getMediatagFromHref(fileData)),
]);
var embedButtons = [{
className: 'cancel',
name: Messages.cancel,
onClick: function () {},
keys: [27]
@ -554,12 +755,20 @@ define([
var url = origin + pathname + '#' + hashes.editHash;
// Share link tab
var link = h('div.cp-share-modal', [
var hasFriends = Object.keys(config.friends || {}).length !== 0;
var friendsList = hasFriends ? getFriendsList(config) : undefined;
var friendsUIClass = hasFriends ? '.cp-share-columns' : '';
var link = h('div.cp-share-modal' + friendsUIClass, [
h('div.cp-share-column', [
h('label', Messages.sharedFolders_share),
h('br'),
hasFriends ? h('p', Messages.share_description) : undefined,
UI.dialog.selectable(url, { id: 'cp-share-link-preview', tabindex: 1 })
]),
friendsList
]);
var linkButtons = [{
className: 'cancel',
name: Messages.cancel,
onClick: function () {},
keys: [27]
@ -791,17 +1000,24 @@ define([
button = $('<span>');
break;
}
var active = $(".cp-toolbar-history:visible").length !== 0;
button = $('<button>', {
title: Messages.historyButton,
title: active ? Messages.history_closeTitle : Messages.historyButton,
'class': "fa fa-history cp-toolbar-icon-history",
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.historyText));
button.toggleClass("active", active);
if (data.histConfig) {
if (active) {
button.click(function () { $(".cp-toolbar-history-close").trigger("click"); });
}
else {
button
.click(common.prepareFeedback(type))
.on('click', function () {
common.getHistory(data.histConfig);
});
}
}
break;
case 'more':
button = $('<button>', {

@ -130,6 +130,12 @@ define([
postMessage("DRIVE_USEROBJECT", data, cb);
};
common.restoreDrive = function (data, cb) {
if (data.sfId) { // Shared folder ID
postMessage('RESTORE_SHARED_FOLDER', data, cb, {
timeout: 5 * 60 * 1000
});
return;
}
postMessage("SET", {
key:['drive'],
value: data

@ -291,13 +291,12 @@ define([
UI.confirm(Messages.contacts_confirmRemoveHistory, function (yes) {
if (!yes) { return; }
sframeChan.query('Q_CLEAR_OWNED_CHANNEL', id, function (e) {
execCommand('CLEAR_OWNED_CHANNEL', id, function (e) {
if (e) {
console.error(e);
UI.alert(Messages.contacts_removeHistoryServerError);
return;
}
clearChannel(id);
});
});
});
@ -618,8 +617,10 @@ define([
};
var onLeave = function (obj) {
var channel = obj.id;
var chan = state.channels[channel];
var data = obj.info;
if (contactsData[data.curvePublic]) {
// XXX Teams: if someone leaves a room, don't remove their data if they're also a friend
if (contactsData[data.curvePublic] && !(chan && chan.isFriendChat)) {
delete contactsData[data.curvePublic];
}
updateStatus(channel);

@ -1,15 +1,19 @@
define([
'/customize/application_config.js',
'/common/common-feedback.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/common-messenger.js',
'/common/outer/mailbox.js',
'/bower_components/nthen/index.js',
], function (Feedback, Hash, Util, nThen) {
'/bower_components/chainpad-crypto/crypto.js',
], function (AppConfig, Feedback, Hash, Util, Messenger, Mailbox, nThen, Crypto) {
// Start migration check
// Versions:
// 1: migrate pad attributes
// 2: migrate indent settings (codemirror)
return function (userObject, cb, progress) {
return function (userObject, cb, progress, store) {
var version = userObject.version || 0;
nThen(function () {
@ -186,6 +190,135 @@ define([
Feedback.send('Migrate-8', true);
userObject.version = version = 8;
}
}).nThen(function () {
if (!AppConfig.migrateFriends) { return; } // XXX
// Migration 9: send our mailbox channel to existing friends
var migrateFriends = function () {
var network = store.network;
var channels = {};
var ctx = {
store: store
};
var myData = Messenger.createData(userObject);
var close = function (chan) {
var channel = channels[chan];
if (!channel) { return; }
try {
channel.wc.leave();
} catch (e) {}
delete channels[chan];
};
var onDirectMessage = function (msg, sender) {
if (sender !== network.historyKeeper) { return; }
var parsed = JSON.parse(msg);
// Metadata msg? we don't care
if ((parsed.validateKey || parsed.owners) && parsed.channel) { return; }
// End of history message, "onReady"
if (parsed.channel && channels[parsed.channel]) {
// History cleared while we were offline
// ==> we asked for an invalid last known hash
if (parsed.error && parsed.error === "EINVAL") {
var histMsg = ['GET_HISTORY', parsed.channel, {}];
network.sendto(network.historyKeeper, JSON.stringify(histMsg))
.then(function () {}, function () {});
return;
}
// End of history
if (parsed.state && parsed.state === 1) {
// Channel is ready and we didn't receive their mailbox channel: send our channel
myData.channel = parsed.channel;
var updateMsg = ['UPDATE', myData.curvePublic, +new Date(), myData];
var cryptMsg = channels[parsed.channel].encrypt(JSON.stringify(updateMsg));
channels[parsed.channel].wc.bcast(cryptMsg).then(function () {}, function (err) {
console.error("Can't migrate this friend", channels[parsed.channel].friend, err);
});
close(parsed.channel);
return;
}
} else if (parsed.channel) {
return;
}
// History message: we only care about "UPDATE" messages
var chan = parsed[3];
if (!chan || !channels[chan]) { return; }
var channel = channels[chan];
var msgIn = channel.decrypt(parsed[4]);
var parsedMsg = JSON.parse(msgIn);
if (parsedMsg[0] === 'UPDATE') {
if (parsedMsg[1] === myData.curvePublic) { return; }
var data = parsedMsg[3];
// If it doesn't contain the mailbox channel, ignore the message
if (!data.notifications) { return; }
// Otherwise we know their channel, we can send them our own
channel.friend.notifications = data.notifications;
myData.channel = chan;
Mailbox.sendTo(ctx, 'UPDATE_DATA', myData, {
channel: data.notifications,
curvePublic: data.curvePublic
}, function (obj) {
if (obj && obj.error) { return void console.error(obj); }
console.log('friend migrated', channel.friend);
});
close(chan);
}
};
network.on('message', function(msg, sender) {
try {
onDirectMessage(msg, sender);
} catch (e) {
console.error(e);
}
});
var friends = userObject.friends || {};
Object.keys(friends).forEach(function (curve) {
if (curve.length !== 44) { return; }
var friend = friends[curve];
// Check if it is already a "new" friend
if (friend.notifications) { return; }
/** Old friend:
* 1. Open the messenger channel
* 2. Check if they sent us their mailbox channel
* 3.a. Yes ==> sent them a mail containing our mailbox channel
* 3.b. No ==> post our mailbox data to the messenger channel
*/
network.join(friend.channel).then(function (wc) {
var keys = Crypto.Curve.deriveKeys(friend.curvePublic, userObject.curvePrivate);
var encryptor = Crypto.Curve.createEncryptor(keys);
channels[friend.channel] = {
wc: wc,
friend: friend,
decrypt: encryptor.decrypt,
encrypt: encryptor.encrypt
};
var cfg = {
lastKnownHash: friend.lastKnownHash
};
var msg = ['GET_HISTORY', friend.channel, cfg];
network.sendto(network.historyKeeper, JSON.stringify(msg))
.then(function () {}, function (err) {
console.error("Can't migrate this friend", friend, err);
});
}, function (err) {
console.error("Can't migrate this friend", friend, err);
});
});
};
if (version < 9) {
migrateFriends();
Feedback.send('Migrate-9', true);
userObject.version = version = 9;
}
/*}).nThen(function (waitFor) {
// Test progress bar in the loading screen
var i = 0;
@ -197,7 +330,7 @@ define([
}, 500);
progress(0, 0);*/
}).nThen(function () {
cb();
setTimeout(cb);
});
};
});

@ -1,12 +1,15 @@
define([
'jquery',
'/common/hyperscript.js',
'/common/common-hash.js',
'/common/common-ui-elements.js',
'/customize/messages.js',
], function ($, h, UIElements, Messages) {
], function ($, h, Hash, UIElements, Messages) {
var handlers = {};
// Friend request
handlers['FRIEND_REQUEST'] = function (common, data, el) {
var content = data.content;
var msg = content.msg;
@ -17,9 +20,9 @@ define([
common.addFriendRequest(data);
// Display the notification
$(el).find('.cp-notification-content').addClass("cp-clickable");
$(el).find('.cp-notification-content p')
.html(Messages._getKey('friendRequest_notification', [msg.content.displayName || Messages.anonymous]))
.html(Messages._getKey('friendRequest_notification', [msg.content.displayName || Messages.anonymous]));
$(el).find('.cp-notification-content').addClass("cp-clickable")
.click(function () {
UIElements.displayFriendRequestModal(common, data);
});
@ -41,6 +44,24 @@ define([
$(el).find('.cp-notification-dismiss').css('display', 'flex');
};
// Share pad
handlers['SHARE_PAD'] = function (common, data, el) {
var content = data.content;
var msg = content.msg;
var type = Hash.parsePadUrl(msg.content.href).type;
var key = type === 'drive' ? 'notification_folderShared' :
(type === 'file' ? 'notification_fileShared' :
'notification_padShared');
$(el).find('.cp-notification-content p')
.html(Messages._getKey(key, [msg.content.name || Messages.anonymous, msg.content.title]));
$(el).find('.cp-notification-content').addClass("cp-clickable")
.click(function () {
common.openURL(msg.content.href);
});
$(el).find('.cp-notification-dismiss').css('display', 'flex');
};
return {
add: function (common, data, el) {
var type = data.content.msg.type;

@ -66,7 +66,7 @@ define([
}
broadcast([clientId], "UPDATE_METADATA");
if (Array.isArray(path) && path[0] === 'profile' && store.messenger) {
store.messenger.updateMyData();
Messaging.updateMyData(store);
}
onSync(cb);
};
@ -85,6 +85,20 @@ define([
cb({});
};
Store.restoreSharedFolder = function (clientId, data, cb) {
if (!data.sfId || !data.drive) { return void cb({error:'EINVAL'}); }
if (store.sharedFolders[data.sfId]) {
Object.keys(data.drive).forEach(function (k) {
store.sharedFolders[data.sfId].proxy[k] = data.drive[k];
});
Object.keys(store.sharedFolders[data.sfId].proxy).forEach(function (k) {
if (data.drive[k]) { return; }
delete store.sharedFolders[data.sfId].proxy[k];
});
}
onSync(cb);
};
Store.hasSigningKeys = function () {
if (!store.proxy) { return; }
return typeof(store.proxy.edPrivate) === 'string' &&
@ -327,15 +341,6 @@ define([
account.note = obj.note;
cb(obj);
});
arePinsSynced(function (err, yes) {
if (!yes) {
resetPins(function (err) {
if (err) { return console.error(err); }
console.log('RESET DONE');
});
}
});
});
});
};
@ -644,7 +649,7 @@ define([
}
store.proxy[Constants.displayNameKey] = value;
broadcast([clientId], "UPDATE_METADATA");
if (store.messenger) { store.messenger.updateMyData(); }
Messaging.updateMyData(store);
onSync(cb);
};
@ -1441,6 +1446,30 @@ define([
};
registerProxyEvents = function (proxy, fId) {
proxy.on('change', [], function (o, n, p) {
if (fId) {
// Pin the new pads
if (p[0] === UserObject.FILES_DATA && typeof(n) === "object" && n.channel && !n.owners) {
var toPin = [n.channel];
// Also pin the onlyoffice channels if they exist
if (n.rtChannel) { toPin.push(n.rtChannel); }
if (n.lastVersion) { toPin.push(n.lastVersion); }
Store.pinPads(null, toPin, function (obj) { console.error(obj); });
}
// Unpin the deleted pads (deleted <=> changed to undefined)
if (p[0] === UserObject.FILES_DATA && typeof(o) === "object" && o.channel && !n) {
var toUnpin = [o.channel];
var c = store.manager.findChannel(o.channel);
var exists = c.some(function (data) {
return data.fId !== fId;
});
if (!exists) { // Unpin
// Also unpin the onlyoffice channels if they exist
if (o.rtChannel) { toUnpin.push(o.rtChannel); }
if (o.lastVersion) { toUnpin.push(o.lastVersion); }
Store.unpinPads(null, toUnpin, function (obj) { console.error(obj); });
}
}
}
sendDriveEvent('DRIVE_CHANGE', {
id: fId,
old: o,
@ -1645,14 +1674,16 @@ define([
});
userObject.migrate(waitFor());
}).nThen(function (waitFor) {
Store.initAnonRpc(null, null, waitFor());
Store.initRpc(null, null, waitFor());
}).nThen(function (waitFor) {
loadMailbox(waitFor);
Migrate(proxy, waitFor(), function (version, progress) {
postMessage(clientId, 'LOADING_DRIVE', {
state: (2 + (version / 10)),
progress: progress
});
});
Store.initAnonRpc(null, null, waitFor());
Store.initRpc(null, null, waitFor());
}).nThen(function (waitFor) {
postMessage(clientId, 'LOADING_DRIVE', {
state: 3
@ -1662,10 +1693,18 @@ define([
loadMessenger();
loadCursor();
loadOnlyOffice();
loadMailbox(waitFor);
loadUniversal(Profile, 'profile', waitFor);
cleanFriendRequests();
}).nThen(function () {
arePinsSynced(function (err, yes) {
if (!yes) {
resetPins(function (err) {
if (err) { return console.error(err); }
console.log('RESET DONE');
});
}
});
var requestLogin = function () {
broadcast([], "REQUEST_LOGIN");
};
@ -1817,8 +1856,8 @@ define([
// Ping clients regularly to make sure one tab was not closed without sending a removeClient()
// command. This allow us to avoid phantom viewers in pads.
var PING_INTERVAL = 30000;
var MAX_PING = 5000;
var PING_INTERVAL = 120000;
var MAX_PING = 30000;
var MAX_FAILED_PING = 2;
setInterval(function () {

@ -144,8 +144,30 @@ define([
cb(true);
};
handlers['UPDATE_DATA'] = function (ctx, box, data, cb) {
var msg = data.msg;
var curve = msg.author;
var friend = ctx.store.proxy.friends && ctx.store.proxy.friends[curve];
if (!friend || typeof msg.content !== "object") { return void cb(true); }
Object.keys(msg.content).forEach(function (key) {
friend[key] = msg.content[key];
});
ctx.updateMetadata();
cb(true);
};
return {
add: function (ctx, box, data, cb) {
/**
* data = {
msg: {
type: 'STRING',
author: 'curvePublicString',
content: {} (depend on the "type")
},
hash: 'string'
}
*/
if (!data.msg) { return void cb(true); }
var type = data.msg.type;

@ -65,7 +65,7 @@ proxy.mailboxes = {
};
// Send a message to someone else
var sendTo = function (ctx, type, msg, user, cb) {
var sendTo = Mailbox.sendTo = function (ctx, type, msg, user, cb) {
if (!Crypto.Mailbox) {
return void cb({error: "chainpad-crypto is outdated and doesn't support mailboxes."});
}

@ -57,6 +57,7 @@ define([
GET_SHARED_FOLDER: Store.getSharedFolder,
ADD_SHARED_FOLDER: Store.addSharedFolder,
LOAD_SHARED_FOLDER: Store.loadSharedFolderAnon,
RESTORE_SHARED_FOLDER: Store.restoreSharedFolder,
// Messaging
ANSWER_FRIEND_REQUEST: Store.answerFriendRequest,
SEND_FRIEND_REQUEST: Store.sendFriendRequest,

@ -152,13 +152,9 @@ define([
if (typeof(channel) !== 'string' || channel.length !== 32) {
return void cb('INVALID_ARGUMENTS');
}
rpc.send('CLEAR_OWNED_CHANNEL', channel, function (e, response) {
rpc.send('CLEAR_OWNED_CHANNEL', channel, function (e) {
if (e) { return cb(e); }
if (response && response.length) {
cb(void 0, response[0]);
} else {
cb('INVALID_RESPONSE');
}
cb();
});
};

@ -357,16 +357,14 @@ define([
UI.removeLoadingScreen(emitResize);
var privateDat = cpNfInner.metadataMgr.getPrivateData();
var hash = privateDat.availableHashes.editHash ||
privateDat.availableHashes.viewHash;
var href = privateDat.pathname + '#' + hash;
var type = privateDat.app;
if (AppConfig.textAnalyzer && textContentGetter) {
AppConfig.textAnalyzer(textContentGetter, privateDat.channel);
}
if (options.thumbnail && privateDat.thumbnails) {
if (hash) {
options.thumbnail.href = href;
if (type) {
options.thumbnail.type = type;
options.thumbnail.getContent = function () {
if (!cpNfInner.chainpad) { return; }
return cpNfInner.chainpad.getUserDoc();

@ -231,7 +231,20 @@ define([
};
var $block = exp.$language = UIElements.createDropdown(dropdownConfig);
$block.find('button').attr('title', Messages.languageButtonTitle);
$block.find('a').click(function () {
var isHovering = false;
var $aLanguages = $block.find('a');
$aLanguages.mouseenter(function () {
isHovering = true;
setMode($(this).attr('data-value'));
});
$aLanguages.mouseleave(function () {
if (isHovering) {
setMode($block.find(".cp-dropdown-element-active").attr('data-value'));
}
});
$aLanguages.click(function () {
isHovering = false;
setMode($(this).attr('data-value'), onModeChanged);
onLocal();
});
@ -272,7 +285,21 @@ define([
setTheme(lastTheme, $block);
$block.find('a').click(function () {
var isHovering = false;
var $aThemes = $block.find('a');
$aThemes.mouseenter(function () {
isHovering = true;
var theme = $(this).attr('data-value');
setTheme(theme, $block);
});
$aThemes.mouseleave(function () {
if (isHovering) {
setTheme(lastTheme, $block);
Common.setAttribute(themeKey, lastTheme);
}
});
$aThemes.click(function () {
isHovering = false;
var theme = $(this).attr('data-value');
setTheme(theme, $block);
Common.setAttribute(themeKey, theme);

@ -63,7 +63,8 @@ define([
var sframeChan = common.getSframeChannel();
sframeChan.query('Q_GET_HISTORY_RANGE', {
lastKnownHash: lastKnownHash
lastKnownHash: lastKnownHash,
sharedFolder: config.sharedFolder
}, function (err, data) {
if (err) { return void console.error(err); }
if (!Array.isArray(data.messages)) { return void console.error('Not an array!'); }
@ -313,6 +314,10 @@ define([
$(window).trigger('resize');
};
if (config.onOpen) {
config.onOpen();
}
// Load all the history messages into a new chainpad object
loadMoreHistory(config, common, function (err, newRt, isFull) {
History.readOnly = common.getMetadataMgr().getPrivateData().readOnly;

@ -5,7 +5,8 @@ define([
'/common/common-ui-elements.js',
'/common/notifications.js',
'/common/hyperscript.js',
], function ($, Util, UI, UIElements, Notifications, h) {
'/customize/messages.js',
], function ($, Util, UI, UIElements, Notifications, h, Messages) {
var Mailbox = {};
Mailbox.create = function (Common) {
@ -48,7 +49,10 @@ define([
};
var createElement = function (data) {
var notif;
var dismiss = h('span.fa.fa-times');
var dismissIcon = h('span.fa.fa-times');
var dismiss = h('div.cp-notification-dismiss', {
title: Messages.notifications_dismiss
}, dismissIcon);
dismiss.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
@ -65,7 +69,7 @@ define([
'data-hash': data.content.hash
}, [
h('div.cp-notification-content', h('p', formatData(data))),
h('div.cp-notification-dismiss', dismiss)
dismiss
]);
return notif;
};
@ -132,9 +136,14 @@ define([
});
};
var subscribed = false;
// Get all existing notifications + the new ones when they come
mailbox.subscribe = function (cfg) {
if (!subscribed) {
execCommand('SUBSCRIBE', null, function () {});
subscribed = true;
}
if (typeof(cfg.onViewed) === "function") {
onViewedHandlers.push(cfg.onViewed);
}
@ -166,10 +175,6 @@ define([
}
});
execCommand('SUBSCRIBE', null, function () {
//console.log('subscribed');
});
return mailbox;
};

@ -19,6 +19,7 @@ define([
var SFrameChannel;
var sframeChan;
var FilePicker;
var Share;
var Messaging;
var Notifier;
var Utils = {
@ -38,6 +39,7 @@ define([
'/common/cryptget.js',
'/common/outer/worker-channel.js',
'/filepicker/main.js',
'/share/main.js',
'/common/common-messaging.js',
'/common/common-notifier.js',
'/common/common-hash.js',
@ -49,7 +51,7 @@ define([
'/customize/application_config.js',
'/common/test.js',
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel,
_FilePicker, _Messaging, _Notifier, _Hash, _Util, _Realtime,
_FilePicker, _Share, _Messaging, _Notifier, _Hash, _Util, _Realtime,
_Constants, _Feedback, _LocalStore, _AppConfig, _Test) {
CpNfOuter = _CpNfOuter;
Cryptpad = _Cryptpad;
@ -57,6 +59,7 @@ define([
Cryptget = _Cryptget;
SFrameChannel = _SFrameChannel;
FilePicker = _FilePicker;
Share = _Share;
Messaging = _Messaging;
Notifier = _Notifier;
Utils.Hash = _Hash;
@ -269,9 +272,6 @@ define([
sessionStorage[Utils.Constants.displayPadCreationScreen];
delete sessionStorage[Utils.Constants.displayPadCreationScreen];
var updateMeta = function () {
// TODO availableHashes in privateData may need updates once we have
// a better privileges workflow
//console.log('EV_METADATA_UPDATE');
var metaObj, isTemplate;
nThen(function (waitFor) {
@ -290,12 +290,12 @@ define([
type: cfg.type || parsed.type
};
var additionalPriv = {
app: parsed.type,
accountName: Utils.LocalStore.getAccountName(),
origin: window.location.origin,
pathname: window.location.pathname,
fileHost: ApiConfig.fileHost,
readOnly: readOnly,
availableHashes: hashes,
isTemplate: isTemplate,
feedbackAllowed: Utils.Feedback.state,
isPresent: parsed.hashData && parsed.hashData.present,
@ -326,6 +326,10 @@ define([
additionalPriv.registeredOnly = true;
}
if (['debug', 'profile'].indexOf(parsed.type) !== -1) {
additionalPriv.hashes = hashes;
}
for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }
if (cfg.addData) {
@ -380,6 +384,27 @@ define([
});
});
sframeChan.on('Q_GET_ATTRIBUTE', function (data, cb) {
Cryptpad.getAttribute(data.key, function (e, data) {
cb({
error: e,
data: data
});
});
});
sframeChan.on('Q_SET_ATTRIBUTE', function (data, cb) {
Cryptpad.setAttribute(data.key, data.value, function (e) {
cb({error:e});
});
});
Cryptpad.mailbox.onEvent.reg(function (data) {
sframeChan.event('EV_MAILBOX_EVENT', data);
});
sframeChan.on('Q_MAILBOX_COMMAND', function (data, cb) {
Cryptpad.mailbox.execCommand(data, cb);
});
};
addCommonRpc(sframeChan);
@ -537,9 +562,12 @@ define([
sframeChan.on('Q_GET_HISTORY_RANGE', function (data, cb) {
var nSecret = secret;
if (cfg.isDrive) {
// Shared folder or user hash or fs hash
var hash = Utils.LocalStore.getUserHash() || Utils.LocalStore.getFSHash();
if (data.sharedFolder) { hash = data.sharedFolder.hash; }
if (hash) {
nSecret = Utils.Hash.getSecrets('drive', hash);
var password = (data.sharedFolder && data.sharedFolder.password) || undefined;
nSecret = Utils.Hash.getSecrets('drive', hash, password);
}
}
var channel = nSecret.channel;
@ -589,20 +617,6 @@ define([
}, href);
});
sframeChan.on('Q_GET_ATTRIBUTE', function (data, cb) {
Cryptpad.getAttribute(data.key, function (e, data) {
cb({
error: e,
data: data
});
});
});
sframeChan.on('Q_SET_ATTRIBUTE', function (data, cb) {
Cryptpad.setAttribute(data.key, data.value, function (e) {
cb({error:e});
});
});
sframeChan.on('Q_DRIVE_GETDELETED', function (data, cb) {
Cryptpad.getDeletedPads(data, function (err, obj) {
if (err) { return void console.error(err); }
@ -711,6 +725,45 @@ define([
initFilePicker(data);
});
// Share modal
var ShareModal = {};
var initShareModal = function (cfg) {
cfg.hashes = hashes;
cfg.password = password;
// cfg.hidden means pre-loading the filepicker while keeping it hidden.
// if cfg.hidden is true and the iframe already exists, do nothing
if (!ShareModal.$iframe) {
var config = {};
config.onShareAction = function (data) {
sframeChan.event('EV_SHARE_ACTION', data);
};
config.onClose = function () {
ShareModal.$iframe.hide();
};
config.data = cfg;
config.addCommonRpc = addCommonRpc;
config.modules = {
Cryptpad: Cryptpad,
SFrameChannel: SFrameChannel,
Utils: Utils
};
ShareModal.$iframe = $('<iframe>', {id: 'sbox-share-iframe'}).appendTo($('body'));
ShareModal.modal = Share.create(config);
} else if (!cfg.hidden) {
ShareModal.modal.refresh(cfg, function () {
ShareModal.$iframe.show();
});
}
if (cfg.hidden) {
ShareModal.$iframe.hide();
return;
}
ShareModal.$iframe.focus();
};
sframeChan.on('EV_SHARE_OPEN', function (data) {
initShareModal(data || {});
});
sframeChan.on('Q_TEMPLATE_USE', function (data, cb) {
Cryptpad.useTemplate(data, Cryptget, cb);
});
@ -879,13 +932,6 @@ define([
Cryptpad.universal.execCommand(data, cb);
});
Cryptpad.mailbox.onEvent.reg(function (data) {
sframeChan.event('EV_MAILBOX_EVENT', data);
});
sframeChan.on('Q_MAILBOX_COMMAND', function (data, cb) {
Cryptpad.mailbox.execCommand(data, cb);
});
Cryptpad.onTimeoutEvent.reg(function () {
sframeChan.event('EV_WORKER_TIMEOUT');
});

@ -121,13 +121,9 @@ define([
return '<script src="' + origin + '/common/media-tag-nacl.min.js"></script>';
};
funcs.getMediatagFromHref = function (obj) {
if (!obj || !obj.hash) { return; }
var data = ctx.metadataMgr.getPrivateData();
var secret;
if (obj) {
secret = Hash.getSecrets('file', obj.hash, obj.password);
} else {
secret = Hash.getSecrets('file', data.availableHashes.fileHash, data.password);
}
var secret = Hash.getSecrets('file', obj.hash, obj.password);
if (secret.keys && secret.channel) {
var key = Hash.encodeBase64(secret.keys && secret.keys.cryptKey);
var hexFileName = secret.channel;
@ -391,12 +387,6 @@ define([
}
};
funcs.isStrongestStored = function () {
var data = ctx.metadataMgr.getPrivateData();
if (data.availableHashes.fileHash) { return true; }
return !data.readOnly || !data.availableHashes.editHash;
};
funcs.setDisplayName = function (name, cb) {
cb = cb || $.noop;
ctx.sframeChan.query('Q_SETTINGS_SET_DISPLAY_NAME', name, cb);
@ -432,6 +422,19 @@ define([
return JSON.parse(JSON.stringify(friendRequests));
};
funcs.getFriends = function () {
var priv = ctx.metadataMgr.getPrivateData();
var friends = priv.friends;
var goodFriends = {};
Object.keys(friends).forEach(function (curve) {
if (curve.length !== 44) { return; }
var data = friends[curve];
if (!data.notifications) { return; }
goodFriends[curve] = friends[curve];
});
return goodFriends;
};
// Feedback
funcs.prepareFeedback = function (key) {
if (typeof(key) !== 'string') { return $.noop; }

@ -304,8 +304,10 @@ MessengerUI, Messages) {
} else if (Common.isLoggedIn() && data.curvePublic && !friends[data.curvePublic]
&& !priv.readOnly) {
if (pendingFriends[data.curvePublic] && pendingFriends[data.curvePublic] > friendTo) {
$('<span>', {'class': 'cp-toolbar-userlist-friend'}).text(Messages.userlist_pending)
.appendTo($rightCol);
$('<button>', {
'class': 'fa fa-hourglass-half cp-toolbar-userlist-button',
'title': Messages.profile_friendRequestSent
}).appendTo($nameSpan);
} else if (friendRequests[data.curvePublic]) {
$('<button>', {
'class': 'fa fa-bell cp-toolbar-userlist-button',
@ -523,23 +525,18 @@ MessengerUI, Messages) {
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the userlist");
}
var metadataMgr = config.metadataMgr;
var origin = config.metadataMgr.getPrivateData().origin;
var pathname = config.metadataMgr.getPrivateData().pathname;
var hashes = metadataMgr.getPrivateData().availableHashes;
var $shareBlock = $('<button>', {
'class': 'fa fa-shhare-alt cp-toolbar-share-button',
title: Messages.shareButton
});
var modal = UIElements.createShareModal({
origin: origin,
pathname: pathname,
hashes: hashes,
common: Common
Common.getSframeChannel().event('EV_SHARE_OPEN', {
hidden: true
});
$shareBlock.click(function () {
UI.openCustomModal(UI.dialog.tabs(modal));
Common.getSframeChannel().event('EV_SHARE_OPEN', {
title: Common.getMetadataMgr().getMetadata().title
});
});
toolbar.$leftside.append($shareBlock);
@ -552,23 +549,19 @@ MessengerUI, Messages) {
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the userlist");
}
var metadataMgr = config.metadataMgr;
var origin = config.metadataMgr.getPrivateData().origin;
var pathname = config.metadataMgr.getPrivateData().pathname;
var hashes = metadataMgr.getPrivateData().availableHashes;
var $shareBlock = $('<button>', {
'class': 'fa fa-shhare-alt cp-toolbar-share-button',
title: Messages.shareButton
});
var modal = UIElements.createFileShareModal({
origin: origin,
pathname: pathname,
hashes: hashes,
common: Common
Common.getSframeChannel().event('EV_SHARE_OPEN', {
hidden: true,
file: true
});
$shareBlock.click(function () {
UI.openCustomModal(UI.dialog.tabs(modal));
Common.getSframeChannel().event('EV_SHARE_OPEN', {
file: true
});
});
toolbar.$leftside.append($shareBlock);

@ -0,0 +1,485 @@
{
"main_title": "CryptPad: Sense Rastre, edició col·laborativa en temps real",
"type": {
"pad": "Text enriquit",
"code": "Codi",
"poll": "Enquesta",
"kanban": "Kanban",
"slide": "Presentació",
"drive": "CryptDrive",
"whiteboard": "Pissarra",
"file": "Fitxer",
"media": "Multimèdia",
"todo": "Tasques",
"contacts": "Contactes",
"sheet": "Full (Beta)"
},
"button_newpad": "Nou document",
"button_newcode": "Nova pàgina de codi",
"button_newpoll": "Nova enquesta",
"button_newslide": "Nova presentació",
"button_newwhiteboard": "Nova pissarra",
"button_newkanban": "Nou tauler Kanban",
"button_newsheet": "Nou full",
"common_connectionLost": "<b>S'ha perdut la connexió amb el servidor</b><br>Fins que la connexió no torni, esteu en mode només de lectura.",
"websocketError": "Hi ha hagut un error en connectar-se al servidor WebSocket...",
"typeError": "Aquest document és incompatible amb l'aplicació seleccionada",
"onLogout": "Esteu fora del vostre compte, {0}cliqueu aquí{1} per iniciar la sessió<br>o premeu <em>Esc</em> per accedir al vostre document en mode només de lectura.",
"wrongApp": "No és possible mostrar el contingut d'aquest document en temps real al vostre navegador. Proveu de tornar a carregar aquesta pàgina.",
"padNotPinned": "Aquest document caducarà després de 3 mesos d'inactivitat, {0}connecteu-vos{1} o {2}registreu-vos{3} per conservar-lo.",
"anonymousStoreDisabled": "L'administració d'aquesta instància de CryptPad ha desactivat l'emmagatzematge pels comptes anònims. Cal que inicieu la sessió per utilitzar el CryptDrive.",
"expiredError": "Aquest document ha caducat i ja no està disponible.",
"deletedError": "La persona que va crear aquest document l'ha esborrat i ja no està disponible.",
"inactiveError": "Donada la seva inactivitat, aquest document s'ha esborrat. Premeu Esc per crear un nou document.",
"chainpadError": "Hi ha hagut un error crític mentre s'actualitzava el vostre contingut. Aquesta pàgina es manté en mode només de lectura per assegurar que no perdreu el que ja heu fet.<br>Premeu <em>Esc</em> per continuar veient aquest document o torneu a carregar la pàgina per provar de continuar editant-lo.",
"invalidHashError": "El document que heu demanat té una adreça URL no vàlida.",
"errorCopy": " Encara podeu copiar el contingut en una altra ubicació prement <em>Esc</em>.<br>Un cop deixeu aquesta pàgina, desapareixerà per sempre!",
"errorRedirectToHome": "Premeu <em>Esc</em> per tornar al vostre CryptDrive.",
"newVersionError": "Hi ha una nova versió disponible de CryptPad.<br><a href='#'>Torneu a carregar</a> la pàgina per utilitzar la versió nova o premeu Esc per accedir al vostre contingut en mode <b>fora de línia</b>.",
"loading": "Carregant...",
"error": "Error",
"saved": "Desat",
"synced": "S'ha desat tot",
"deleted": "Document esborrat del vostre CryptDrive",
"deletedFromServer": "Document esborrat del servidor",
"mustLogin": "Cal que inicieu la sessió per accedir a aquesta pàgina",
"disabledApp": "Aquesta aplicació està dehabilitada. Per a més informació, contacteu l'administració d'aquest CryptPad.",
"realtime_unrecoverableError": "Hi ha hagut un error irreparable. Cliqueu D'acord per tornar a carregar la pàgina.",
"disconnected": "S'ha desconnectat",
"synchronizing": "Sincronitzant",
"reconnecting": "Reconnectant",
"typing": "Edició",
"initializing": "Inicialitzant...",
"forgotten": "Desplaçat a la brossa",
"errorState": "Error crític: {0}",
"lag": "Latència",
"readonly": "Només de lectura",
"anonymous": "Anònim",
"yourself": "Vós mateix/a",
"anonymousUsers": "persones editores anònimes",
"anonymousUser": "persona editora anònima",
"users": "Persones usuàries",
"and": "I",
"viewer": "persona lectora",
"viewers": "persones lectores",
"editor": "persona editora",
"editors": "persones editores",
"userlist_offline": "Esteu fora de línia, la llista de persones usuàries no està disponible.",
"language": "Llengua",
"comingSoon": "Ben aviat...",
"newVersion": "<b>S'ha actualitzat CryptPad!</b><br>Comproveu les novetats d'aquesta versió:<br><a href=\"https://github.com/xwiki-labs/cryptpad/releases/tag/{0}\" target=\"_blank\">Notes de la versió de CryptPad {0}</a>",
"upgrade": "Actualitzeu-vos",
"upgradeTitle": "Actualitzeu el vostre compte per obtenir més espai",
"upgradeAccount": "Actualitzeu el compte",
"MB": "MB",
"GB": "GB",
"KB": "KB",
"supportCryptpad": "Doneu suport a CryptPad",
"formattedMB": "{0} MB",
"formattedGB": "{0} GB",
"formattedKB": "{0} KB",
"greenLight": "Tot funciona correctament",
"orangeLight": "La vostra connexió és lenta i pot afectar el funcionament",
"redLight": "Us heu desconnectat de la sessió",
"pinLimitReached": "Heu arribat al límit del vostre emmagatzematge",
"pinLimitReachedAlert": "Heu arribat al límit del vostre espai. Els documents nous no es desaran al vostre CryptDrive.<br>Podeu eliminar documents del vostre CryptDrive o <a href=\"https://accounts.cryptpad.fr/#!on={0}\" target=\"_blank\">adquirir una oferta prèmium</a> per augmentar el vostre espai.",
"pinLimitReachedAlertNoAccounts": "Heu arribat al límit del vostre espai",
"pinLimitNotPinned": "Heu arribat al límit del vostre espai.<br>Aquest document no està desat al vostre CryptDrive.",
"pinLimitDrive": "Heu arribat al límit del vostre espai.<br>No podeu crear documents nous.",
"moreActions": "Més accions",
"importButton": "Importar",
"importButtonTitle": "Importar document des d'un fitxer local",
"exportButton": "Exportar",
"exportButtonTitle": "Exportar aquest document a un fitxer local",
"exportPrompt": "Com voleu anomenar el fitxer?",
"changeNamePrompt": "Canvieu el vostre nom (deixeu-ho en blanc per fer-lo anònim):· ",
"user_rename": "Canvieu el nom visible",
"user_displayName": "Nom visible",
"user_accountName": "Identificador",
"clickToEdit": "Cliqueu per editar",
"saveTitle": "Deseu el títol (Enter)",
"forgetButton": "Esborrar",
"forgetButtonTitle": "Moveu aquest document a la paperera",
"forgetPrompt": "Clicant D'acord, moureu aquest document a la paperera. Hi esteu d'acord?",
"movedToTrash": "Aquest document s'ha desplaçat a la paperera.<br><a href=\"/drive/\">Accedir a la meva Unitat</a>",
"shareButton": "Compartir",
"shareSuccess": "L'enllaç s'ha copiat al porta-retalls",
"userListButton": "Llista de persones usuàries",
"chatButton": "Xat",
"userAccountButton": "El vostre compte",
"newButton": "Nou/Nova",
"newButtonTitle": "Crea un nou document",
"uploadButton": "Carregar fitxers",
"uploadButtonTitle": "Carrega un fitxer nou a la carpeta actual",
"saveTemplateButton": "Desa com una plantilla",
"saveTemplatePrompt": "Trieu un títol per la plantilla",
"templateSaved": "Plantilla desada!",
"selectTemplate": "Escolliu una plantilla o premeu ESC",
"useTemplate": "Voleu començar amb una plantilla?",
"useTemplateOK": "Escolliu una plantilla (Enter)",
"useTemplateCancel": "Reinicieu (Esc)",
"template_import": "Importeu una plantilla",
"template_empty": "No hi ha plantilles disponibles",
"previewButtonTitle": "Mostra o amaga el mode de previsualització Markdown",
"presentButtonTitle": "Entra en el mode presentació",
"backgroundButtonTitle": "Canvieu el color de fons de la presentació",
"colorButtonTitle": "Canvieu el color del text en el mode presentació",
"propertiesButton": "Propietats",
"propertiesButtonTitle": "Mostra les propietats del document",
"printText": "Imprimeix",
"printButton": "Imprimeix (Enter)",
"printButtonTitle2": "Imprimeix el document o exporta'l com un fitxer PDF",
"printOptions": "Opcions de la compaginació",
"printSlideNumber": "Mostra el número de diapositiva",
"printDate": "Mostra la data",
"printTitle": "Mostra el títol del document",
"printCSS": "Normes d'estil personalitzades (CSS):",
"printTransition": "Activa les transicions animades",
"printBackground": "Utilitza una imatge de fons",
"printBackgroundButton": "Escolliu una imatge",
"printBackgroundValue": "<b>Fons actual:</b> <em>{0}</em>",
"printBackgroundNoValue": "<em>No es mostra cap imatge de fons</em>",
"printBackgroundRemove": "Suprimeix aquesta imatge de fons",
"filePickerButton": "Incrusta un fitxer desat al CryptDrive",
"filePicker_close": "Tanca",
"filePicker_description": "Trieu un fitxer del vostre CryptDrive per incrustar-lo o carregueu-ne un de nou",
"filePicker_filter": "Filtra els fitxers pel nom",
"or": "o",
"tags_title": "Etiquetes (només vostres)",
"tags_add": "Actualitza les etiquetes d'aquesta pàgina",
"tags_searchHint": "Inicieu una cerca amb # al vostre CryptDrive per trobar els vostres documents etiquetats.",
"tags_notShared": "Les vostres etiquetes no es comparteixen amb altres persones usuàries",
"tags_duplicate": "Etiquetes duplicades: {0}",
"tags_noentry": "No podeu etiquetar un document esborrat!",
"slideOptionsText": "Opcions",
"slideOptionsTitle": "Personalitzeu les vostres diapositives",
"slideOptionsButton": "Desa (Enter)",
"slide_invalidLess": "L'estil personalitzat no és vàlid",
"languageButton": "Llengua",
"languageButtonTitle": "Escolliu la llengua que voleu utilitzar per marcar la sintaxi",
"themeButton": "Tema",
"themeButtonTitle": "Escolliu el color del tema a utilitzar als editors de codi i diapositives",
"editShare": "Enllaç d'edició",
"editShareTitle": "Copia l'enllaç d'edició al porta-retalls",
"editOpen": "Obre l'enllaç d'edició en una perstanya nova",
"editOpenTitle": "Obre aquest document en mode \"edició\" en una pestanya nova",
"viewShare": "Enllaç només de lectura",
"viewShareTitle": "Copia l'enllaç només de lectura al porta-retalls",
"viewOpen": "Obre l'enllaç només de lectura en una pestanya nova",
"viewOpenTitle": "Obre aquest document en mode només de lectura en una pestanya nova",
"fileShare": "Copia l'enllaç",
"getEmbedCode": "Obté el codi d'incrustat",
"viewEmbedTitle": "Inscrusta el document en una pàgina externa",
"viewEmbedTag": "Per incrustar aquest document, poseu aquest iframe allà on vulgueu de la vostra pàgina. Podeu donar-li l'estil desitjat utilitzant els atributs CSS o HTML.",
"fileEmbedTitle": "Incrusta el fitxer en una pàgina externa",
"fileEmbedScript": "Per incrustar aquest fitxer, poseu aquest script un cop a la vostra pàgina per carregar l'Etiqueta Multimèdia:",
"fileEmbedTag": "Llavors, col·loqueu aquesta Etiqueta Multimèdia al lloc vulgueu incrustar-lo de la vostra pàgina:",
"notifyJoined": "{0} s'ha unit a la sessió col·laborativa",
"notifyRenamed": "{0} es coneix ara com {1}",
"notifyLeft": "{0} ha deixat la sessió col·laborativa",
"ok": "D'acord",
"okButton": "D'acord (Enter)",
"cancel": "Cancel·la",
"cancelButton": "Cancel·la (Esc)",
"doNotAskAgain": "No ho preguntis més (Esc)",
"show_help_button": "Mostra l'ajuda",
"hide_help_button": "Amaga l'ajuda",
"help_button": "Ajuda",
"historyText": "Historial",
"historyButton": "Mostra l'historial del document",
"history_next": "La versió més nova",
"history_prev": "La versió més antiga",
"history_loadMore": "Carrega més historial",
"history_closeTitle": "Tanca l'historial",
"history_restoreTitle": "Restaura la versió seleccionada del document",
"history_restorePrompt": "Segur que voleu reemplaçar la versió actual del document per la que es mostra?",
"history_restoreDone": "S'ha restaurat el document",
"history_version": "Versió:",
"openLinkInNewTab": "Obre l'enllaç en una pestanya nova",
"pad_mediatagTitle": "Configuració de l'Etiqueta Multimèdia",
"pad_mediatagWidth": "Amplada (px)",
"pad_mediatagHeight": "Alçada (px)",
"pad_mediatagRatio": "Mantenir la proporció",
"pad_mediatagBorder": "Amplada de la vora (px)",
"pad_mediatagPreview": "Previsualització",
"pad_mediatagImport": "Deseu-ho al vostre CryptDrive",
"pad_mediatagOptions": "Propietats de la imatge",
"kanban_newBoard": "Taulell nou",
"kanban_item": "Element {0}",
"kanban_todo": "A fer",
"kanban_done": "Completes",
"kanban_working": "En procés",
"kanban_deleteBoard": "De debò voleu eliminar aquest taulell?",
"kanban_addBoard": "Afegeix un taulell",
"kanban_removeItem": "Suprimeix aquest element",
"kanban_removeItemConfirm": "De debò voleu esborrar aquest element?",
"poll_title": "Sense saber-ne Selector de data",
"poll_subtitle": "Sense saber-ne, planificació a <em>temps real</em>",
"poll_p_save": "La vostra configuració es carrega instantàniament, per tant, mai us caldrà desar-la.",
"poll_p_encryption": "Tot el que introduïu s'encripta pel que només les persones que tenen l'enllaç poden accedir-hi. Fins i tot el servidor no pot veure el que canvieu.",
"wizardLog": "Cliqueu el botó de dalt a l'esquerra per tornar a l'enquesta",
"wizardTitle": "Utilitzeu l'assistent per crear la vostra enquesta",
"wizardConfirm": "Esteu a punt per afegir aquestes opcions a l'enquesta?",
"poll_publish_button": "Publica",
"poll_admin_button": "Administra",
"poll_create_user": "Afegeix una persona",
"poll_create_option": "Afegeix una opció",
"poll_commit": "Envia",
"poll_closeWizardButton": "Tanca l'assistent",
"poll_closeWizardButtonTitle": "Tanca l'assistent",
"poll_wizardComputeButton": "Computa les opcions",
"poll_wizardClearButton": "Buida la taula",
"poll_wizardDescription": "Crea automàticament una quantitat d'opcions, introduint qualsevol nombre de segments de dates i hores",
"poll_wizardAddDateButton": "+ Dates",
"poll_wizardAddTimeButton": "+ Hores",
"poll_optionPlaceholder": "Opció",
"poll_userPlaceholder": "El vostre nom",
"poll_removeOption": "De debò voleu suprimir aquesta opció?",
"poll_removeUser": "De debò voleu suprimir aquesta persona?",
"poll_titleHint": "Títol",
"poll_descriptionHint": "Descriviu la vostra enquesta i utilitzeu el botó ✓ (publica) quan ho hàgiu enllestit.\nLa descripció es pot escriure ustilitzant la sintaxi Markdown i podeu incrustar-hi elements multimèdia des del vostre CryptDrive.\nQualsevol persona que tingui l'enllaç pot canviar la descripció, però això es descoratjador.",
"poll_remove": "Suprimeix",
"poll_edit": "Edita",
"poll_locked": "Blocat",
"poll_unlocked": "Desblocat",
"poll_bookmark_col": "Destaqueu aquesta columna per a què, des del principi, la tingueu sempre desblocada i a la vista",
"poll_bookmarked_col": "Aquesta és la vostra columna destacada. Des del principi sempre la tindreu desblocada i a la vista.",
"poll_total": "TOTAL",
"poll_comment_list": "Comentaris",
"poll_comment_add": "Afegiu un comentari",
"poll_comment_submit": "Envia",
"poll_comment_remove": "Esborra aquest comentari",
"poll_comment_placeholder": "El vostre comentari",
"poll_comment_disabled": "Publiqueu aquesta enquesta utilitzant el botó ✓ per habilitar els comentaris.",
"oo_reconnect": "La connexió amb el servidor ha caigut. Cliqueu D'acord per tornar a carregar la pàgina i continuar amb l'edició.",
"oo_cantUpload": "No està permesa la càrrega mentre hi hagi altres persones presents.",
"oo_uploaded": "S'ha completat la vostra càrrega. Cliqueu D'acord per tornar a carregar la pàgina o Cancel·la per continuar en el mode només de lectura.",
"canvas_clear": "Neteja",
"canvas_delete": "Esborra la selecció",
"canvas_disable": "Deshabilita el dibuix",
"canvas_enable": "Habilita el dibuix",
"canvas_width": "Amplada",
"canvas_opacity": "Transparència",
"canvas_opacityLabel": "Transparència: {0}",
"canvas_widthLabel": "Amplada: {0}",
"canvas_saveToDrive": "Deseu aquesta imatge com un fitxer al vostre CryptDrive",
"canvas_currentBrush": "Pinzell actual",
"canvas_chooseColor": "Trieu un color",
"canvas_imageEmbed": "Incrusteu una imatge des del vostre dispositiu",
"profileButton": "Perfil",
"profile_urlPlaceholder": "Adreça URL",
"profile_namePlaceholder": "Nom visible en el vostre perfil",
"profile_avatar": "Avatar",
"profile_upload": " Carregueu un nou avatar",
"profile_uploadSizeError": "Error: l'avatar cal que sigui més petit que {0}",
"profile_uploadTypeError": "Error: el tipus d'avatar no està permès. Els tipus permesos són: {0}",
"profile_error": "Hi ha hagut un error mentre es creava el vostre perfil: {0}",
"profile_register": "Cal que us registreu per crear un perfil!",
"profile_create": "Crea un perfil",
"profile_description": "Descripció",
"profile_fieldSaved": "S'ha desat un nou valor: {0}",
"profile_viewMyProfile": "Veure el meu perfil",
"userlist_addAsFriendTitle": "Envia una sol·licitud de contacte a {0}",
"contacts_title": "Contactes",
"contacts_addError": "Hi ha hagut un error mentre s'afegia aquest contacte a la llista",
"contacts_added": "Invitació de contacte acceptada.",
"contacts_rejected": "Invitació de contacte rebutjada",
"contacts_request": "<em>{0}</em> vol afegir-vos com a contacte. <b>Ho accepteu<b>?",
"contacts_send": "Envia",
"contacts_remove": "Suprimeix aquest contacte",
"contacts_confirmRemove": "De debò voleu suprimir <em>{0}</em> dels vostres contactes?",
"contacts_typeHere": "Escriviu un missatge aquí...",
"contacts_warning": "Qualsevol cosa que escriviu aquí, restarà disponible per qualsevol persona usuària d'aquest document. Aneu en compte amb la informació sensible!",
"contacts_padTitle": "Xat",
"contacts_info1": "Aquests són els vostres contactes. Des d'aquí podeu:",
"contacts_info2": "Cliqueu la icona del contacte que vulgueu per xatejar-hi",
"contacts_info3": "Feu doble clic la seva icona per veure el seu perfil",
"contacts_info4": "Qualsevol de les persones participants pot fer neteja permanent de l'hitorial de xat",
"contacts_removeHistoryTitle": "Neteja l'historial del xat",
"contacts_confirmRemoveHistory": "De debò voleu suprimir permanentment el vostre historial de xat? Les dades no es podran restaurar",
"contacts_removeHistoryServerError": "Hi ha hagut un error mentre es suprimia el vostre historial del xat. Torneu-ho a provar",
"contacts_fetchHistory": "Recupera l'historial antic",
"contacts_friends": "Amistats",
"contacts_rooms": "Sales",
"contacts_leaveRoom": "Deixa aquesta sala",
"contacts_online": "En aquesta sala hi ha una altra persona en línia",
"debug_getGraph": "Aconseguiu el codi per generar una gràfica d'aquest document",
"debug_getGraphWait": "Generant la gràfica... Si us plau, espereu.",
"debug_getGraphText": "Aquest és el codi DOT per generar una gràfica de l'historial d'aquest document:",
"fm_rootName": "Documents",
"fm_trashName": "Paperera",
"fm_unsortedName": "Fitxers desordenats",
"fm_filesDataName": "Tots els fitxers",
"fm_templateName": "Plantilles",
"fm_searchName": "Cerca",
"fm_recentPadsName": "Documents recents",
"fm_ownedPadsName": "Propi",
"fm_tagsName": "Etiquetes",
"fm_sharedFolderName": "Carpeta compartida",
"fm_searchPlaceholder": "Cercant...",
"fm_newButton": "Nou",
"fm_newButtonTitle": "Crea un nou document o carpeta, importa un fitxer a la carpeta actual",
"fm_newFolder": "Carpeta nova",
"fm_newFile": "Document de text nou",
"fm_folder": "Carpeta",
"fm_sharedFolder": "Carpeta compartida",
"fm_folderName": "Nom de la carpeta",
"fm_numberOfFolders": "# de carpetes",
"fm_numberOfFiles": "# de fitxers",
"fm_fileName": "Nom de fitxer",
"fm_title": "Títol",
"fm_type": "Tipus",
"fm_lastAccess": "Darrer accés",
"fm_creation": "Creat/da",
"fm_forbidden": "Acció prohibida",
"fm_originalPath": "Camí original",
"fm_openParent": "Veure a la carpeta",
"fm_noname": "Document sense títol",
"fm_emptyTrashDialog": "De debò voleu buidar la paperera?",
"fm_removeSeveralPermanentlyDialog": "De debò voleu suprimir permanentment aquests {0} elements del vostre CryptDrive?",
"fm_removePermanentlyNote": "Si continueu, els vostres propis documents se suprimiran del servidor.",
"fm_removePermanentlyDialog": "De debò voleu suprimir permanentment aquest element del vostre CryptDrive?",
"fm_removeSeveralDialog": "De debò voleu desplaçar permanentment aquests {0} elements a la paperera?",
"fm_removeDialog": "De debò voleu desplaçar {0} a la paperera?",
"fm_deleteOwnedPad": "De debò voleu suprimir permanentment aquest document del servidor?",
"fm_deleteOwnedPads": "De debò voleu suprimir permanentment aquests documents del servidor?",
"fm_restoreDialog": "De debò voleu restaurar {0} a la seva ubicació prèvia?",
"fm_unknownFolderError": "La carpeta visitada o seleccionada ja no existeix. Obrint la carpeta superior...",
"fm_contextMenuError": "No es pot obrir el menú contextual d'aquest element. Si el problema continua, proveu de tornar a carregar la pàgina.",
"fm_selectError": "No es pot seleccionar l'element objectiu. Si el problema continua, proveu de tornar a carregar la pàgina.",
"fm_categoryError": "No es pot obrir la categoria seleccionada, mostrant l'arrel.",
"fm_info_root": "Creeu aquí tantes carpetes imbrincades com vulgueu per ordenar els vostres fitxers.",
"fm_info_unsorted": "Conté tots els fitxers que heu visitat i que encara no estan ordenats, a \"Documents\" o desplaçats a la \"Paperera\".",
"fm_info_template": "Conté tots els documents desats com plantilles i que podeu reutilitzar quan vulgueu crear un nou document.",
"fm_info_recent": "Llista els documents modificats o oberts recentment.",
"fm_info_trash": "Buideu la paperera per alliberar espai al vostre CryptDrive.",
"fm_info_allFiles": "Conté tots els fitxers de \"Documents\", \"Desordenats\" i \"Paperera\". No podeu desplaçar o suprimir fitxers des d'aquí.",
"fm_info_anonymous": "No heu iniciat la sessió, per tant, els vostres documents caducaran d'aquí a 3 mesos (<a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">saber-ne més</a>). Es desen al vostre navegador, per tant, si netegeu el vostre historial podríeu perdre'ls.<br><a href=\"/register/\">Registreu-vos</a> o <a href=\"/login/\">Inicieu la sessió</a> per mantenir-los accessibles.<br>",
"fm_info_sharedFolder": "Aquesta és una carpeta compartida. No heu iniciat cap sessió, pel que només podeu accedir en mode només de lectura.<br><a href=\"/register/\">Registreu-vos</a> o <a href=\"/login/\">Inicieu la sessió</a> per poder importar-ho al vostre CryptDrive i modificar-ho.",
"fm_info_owned": "Es documents que es mostren són de la vostra propietat. Això vol dir que podeu eliminar-los permanentment del servidor quan vulgueu. Si ho feu, la resta de persones no podran accedir-hi mai més.",
"fm_alert_backupUrl": "Enllaç de còpia de seguretat d'aquesta unitat.<br>És <strong>altament recomanat</strong> que el manteniu en secret.<br>Podeu utilitzar-lo per recuperar tots els vostres fitxers en el cas que la memòria del vostre navegador sigui esborrada.<br>Qualsevol persona que tingui l'enllaç pot editar o suprimir tots els fitxers en el vostre gestor de fitxers.<br>",
"fm_alert_anonymous": "Hola, esteu utilitzant CryptPad de forma anònima. Ja està bé, però els vostres documents poden perdre's després d'un temps d'inactivitat. Hem deshabilitat les funcions avançades de la unitat pels comptes anònims perquè volem deixar clar que aquest no és un lloc segur on desar res. Podeu <a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">conèixer més</a> sobre per què fem això i per què hauríeu de <a href=\"/register/\">Registrar-vos</a> i <a href=\"/login/\">Iniciar sessió</a>.",
"fm_backup_title": "Enllaç de còpia de seguretat",
"fm_nameFile": "Com voldreu anomenar aquest fitxer?",
"fm_error_cantPin": "Error intern del servidor. Si us plau, torneu a garregar la pàgina i torneu a provar-ho.",
"fm_viewListButton": "Vista de llista",
"fm_viewGridButton": "Vista de graella",
"fm_renamedPad": "Heu establert un nom personalitzat per aquest document. El seu títol compartit és:<br><b>{0}</b>",
"fm_canBeShared": "Podeu compartir aquesta carpeta",
"fm_prop_tagsList": "Etiquetes",
"fm_burnThisDriveButton": "Esborra tota la informació desada per CryptPad al vostre navegador",
"fm_burnThisDrive": "De debò voleu suprimir tot el que CryptPad ha desat al vostre navegador?<br>Això suprimirà el vostre CryptDrive i el seu historial ubicat al navegador, però els vostres documents continuaran existint (encriptats) en el nostre servidor.",
"fm_padIsOwned": "Aquest document és de la vostra propietat",
"fm_padIsOwnedOther": "Aquest document és propietat d'una altra persona",
"fm_deletedPads": "Aquests documents deixaran d'existir al servidor, es suprimiran del vostre CryptDrive: {0}",
"fm_tags_name": "Nom d'etiqueta",
"fm_tags_used": "Nombre d'usos",
"fm_restoreDrive": "Reconfigura la vostra unitat a un estat anterior. Per uns bons resultats, eviteu fer canvis a la unitat fins que el procés s'hagi completat.",
"fm_moveNestedSF": "No podeu col·locar una carpeta compartida dins d'una altra. La carpeta {0} no s'ha desplaçat.",
"fm_passwordProtected": "Aquest document està protegit amb contrasenya",
"fc_newfolder": "Carpeta nova",
"fc_newsharedfolder": "Carpeta compartida nova",
"fc_rename": "Reanomena",
"fc_open": "Obre",
"fc_open_ro": "Obre (només de lectura)",
"fc_delete": "Desplaça a la paperera",
"fc_delete_owned": "Elimina del servidor",
"fc_restore": "Restaura",
"fc_remove": "Suprimeix del vostre CryptDrive",
"fc_remove_sharedfolder": "Suprimeix",
"fc_empty": "Buida la paperera",
"fc_prop": "Propietats",
"fc_hashtag": "Etiquetes",
"fc_sizeInKilobytes": "Mida en kilobytes",
"fo_moveUnsortedError": "No podeu desplaçar una carpeta a la llista de plantilles",
"fo_existingNameError": "El nom ja existeix en aquesta carpeta. Si us plau, trieu un altre.",
"fo_moveFolderToChildError": "No podeu desplaçar una carpeta dins d'una de les seves descendents",
"fo_unableToRestore": "No podeu restaurar aquest fitxer a la seva ubicació original. Podeu provar de desplaçar-lo a una unova ubicació.",
"fo_unavailableName": "Ja existeix un fitxer o carpeta amb el mateix nom a la ubicació nova. Reanomeneu l'element i proveu-ho un altre cop.",
"fs_migration": "El vostre CryptDrive s'ha actualitzat a una nova versió. Com a resultat, la pàgina actual s'ha de tornar a carregar.<br><strong>Si us plau, torneu a carregar la pàgina per seguir utilitzant-la.</strong>",
"login_login": "Inicia la sessió",
"login_makeAPad": "Crea un document anònimament",
"login_nologin": "Veure els documents locals",
"login_register": "Registra",
"logoutButton": "Tanca la sessió",
"settingsButton": "Configuració",
"login_username": "Identificador personal",
"login_password": "Contrasenya",
"login_confirm": "Confirmeu la contrasenya",
"login_remember": "Recordeu-me",
"login_hashing": "Realitzant la funció resum a la contrasenya, pot portar una bona estona.",
"login_hello": "Hola {0},",
"login_helloNoName": "Hola,",
"login_accessDrive": "Accediu a la vostra unitat",
"login_orNoLogin": "o",
"login_noSuchUser": "L'identificador o la contrasenya no són vàlids. Torneu-ho a provar o registreu-vos",
"login_invalUser": "Cal introduir l'identificador",
"login_invalPass": "Cal introduir la contrasenya",
"login_unhandledError": "Hi ha hagut un error inesperat :(",
"register_importRecent": "Importeu els documents de la vostra sessió anònima",
"register_acceptTerms": "Accepto les <a href='/terms.html' tabindex='-1'>condicions del servei</a>",
"register_passwordsDontMatch": "Les contrasenyes no coincideixen!",
"register_passwordTooShort": "Les contrasenyes han de tenir, com a mínim, {0} caràcters.",
"register_mustAcceptTerms": "Cal que accepteu les condicions del servei.",
"register_mustRememberPass": "No podem restablir la contrasenya si l'oblideu. És molt important que la recordeu! Marqueu la casella per confirmar que ho enteneu.",
"register_whyRegister": "Per què cal registrar-se?",
"register_header": "Us donem la benvinguda a CryptPad",
"register_explanation": "<h3>Abans de començar, veurem algunes coses:</h3><ul class='list-unstyled'><li><i class='fa fa-info-circle'></i>La contrasenya és la clau secreta que encripta tots els vostres documents. Si la perdeu, no hi ha manera de recuperar les vostres dades.</li><li><i class='fa fa-info-circle'></i>Podeu importar documents que hàgiu vist al vostre navegador per tenir-los al vostre compte.</li><li><i class='fa fa-info-circle'></i>Si utilitzeu un ordinador compartit, cal que tanqueu la sessió quan acabeu de treballar, tancar la pestanya no és suficient.</li></ul>",
"register_writtenPassword": "He introduït el meu identificador i la contrasenya, continua",
"register_cancel": "Torna",
"register_warning": "Sense Rastre significa que no podem recuperar les vostres dades si perdeu o oblideu la vostra contrasenya.",
"register_alreadyRegistered": "Aquest identificador ja existeix, voleu iniciar la sessió?",
"settings_cat_account": "Compte",
"settings_cat_drive": "CryptDrive",
"settings_cat_cursor": "Cursor",
"settings_cat_code": "Codi",
"settings_cat_pad": "Text enriqu¡t",
"settings_cat_creation": "Nou document",
"settings_cat_subscription": "Subscripció",
"settings_title": "Configuració",
"settings_save": "Desa",
"settings_backupCategory": "Còpia de seguretat",
"settings_backupHint": "Deseu o recupereu tot el contingut del vostre CryptDrive. No es desarà el contingut dels vostres documents, només les claus per accedir-hi.",
"settings_backup": "Còpia de seguretat",
"settings_restore": "Recupera",
"settings_backupHint2": "Descarregueu el contingut actual de tots els vostres documents. Els documents es descarregaran en un format llegible, sempre que el format estigui disponible.",
"settings_backup2": "Descarrega el meu CryptDrive",
"settings_backup2Confirm": "Això descarregarà tots els documents i fitxers des del vostre CryptDrive. Si voleu continuar, seleccioneu un nom i premeu D'acord",
"settings_exportTitle": "Exporta el vostre CryptDrive",
"settings_exportDescription": "Si us plau, espereu mentre descarreguem i desencriptem els vostres documents. Pots durar uns minuts. Tanqueu aquesta pestanya si voleu interrompre el procés.",
"settings_exportFailed": "Si un document necessita més d'1 minut per descarregar-se, no s'inclourà a l'exportació. Es mostrarà un enllaç amb qualsevol document que no s'hagi exportat.",
"settings_exportWarning": "Nota: aquesta eina està en versió beta i pot donar problemes d'escalabilitat. Per un millor rendiment, es recomana no canviar de pestanya.",
"settings_exportCancel": "De debò voleu cancel·lar l'exportació? El pròxim cop haureu de tornar a començar des del principi.",
"settings_export_reading": "Estem llegint el vostre CryptDrive...",
"settings_export_download": "Estem descarregant i desencriptant els vostres documents...",
"settings_export_compressing": "Comprimint les dades...",
"settings_export_done": "La vostra descàrrega està llesta!",
"settings_exportError": "Veure els errors",
"settings_exportErrorDescription": "No podem afegir els següents documents a l'exportació:",
"settings_exportErrorEmpty": "Aquest document no es pot exportar (és buit o el contingut no és vàlid).",
"settings_exportErrorMissing": "Aquest document ha desaparegut dels nostres servidors (ha caducat o ha estat esborrat per la persona propietària)",
"settings_exportErrorOther": "Hi ha hagut un error mentre s'intentava exportar aquest document: {0}",
"settings_resetNewTitle": "Neteja CryptDrive",
"settings_resetButton": "Suprimeix",
"settings_reset": "Suprimeix tots els fitxers i carpetes del vostre CryptDrive",
"settings_resetPrompt": "Aquesta acció suprimirà tots els documents de la vostra unitat.<br>De debò voleu continuar?<br>Introduïu \"<em>M'agrada CryptPad</em>\" per confirmar-ho.",
"settings_resetDone": "S'ha buidat la vostra unitat!",
"settings_resetError": "Text de verificació incorrecte. El vostre CryptDrive no s'ha modificat.",
"settings_resetTipsAction": "Restableix",
"settings_resetTips": "Consells",
"settings_resetTipsButton": "Restabliu els consells disponibles al CryptDrive",
"settings_resetTipsDone": "Ara els consells tornen a ser visibles.",
"settings_thumbnails": "Miniatures",
"settings_disableThumbnailsAction": "Deshabilita la creació de miniatures al CryptDrive",
"settings_disableThumbnailsDescription": "Les miniatures es creen i es desen automàticament al vostre navegador quan visiteu un nou document. Podeu deshabilitar aquesta funció des d'aquí.",
"settings_resetThumbnailsAction": "Neteja",
"settings_resetThumbnailsDescription": "Neteja totes les miniatures desades al navegador.",
"settings_resetThumbnailsDone": "S'han esborrat totes les miniatures.",
"settings_importTitle": "Importa aquests documents del navegador al CryptDrive",
"settings_import": "Importa",
"settings_importConfirm": "De debò voleu importar els documents recents des del navegador al vostre compte de CryptDrive?",
"settings_importDone": "S'ha acabat la importació",
"settings_autostoreTitle": "Emmagatzematge de documents a CryptDrive",
"edit": "Edita",
"autostore_sf": "Carpeta"
}

@ -274,13 +274,8 @@
"profile_create": "Ein Profil erstellen",
"profile_description": "Beschreibung",
"profile_fieldSaved": "Neuer Wert gespeichert: {0}",
"profile_inviteButton": "Sich in Verbindung setzen",
"profile_inviteButtonTitle": "Einen Link erstellen, mit dem du diesen Benutzer einladen kannst, sich mit dir in Verbindung zu setzen.",
"profile_inviteExplanation": "Ein Klick auf <strong>OK</strong> wird einen Link erstellen, der eine sichere Chatsitzung <em>nur mit {0}</em> erlaubt.<br></br>Dieser Link kann öffentlich geteilt werden.",
"profile_viewMyProfile": "Mein Profil anzeigen",
"userlist_addAsFriendTitle": "Benutzer \"{0}\" eine Freundschaftsanfrage senden",
"userlist_thisIsYou": "Das bist du (\"{0}\")",
"userlist_pending": "Warte...",
"contacts_title": "Kontakte",
"contacts_addError": "Fehler bei dem Hinzufügen des Kontakts zur Liste",
"contacts_added": "Verbindungseinladung angenommen.",
@ -1066,5 +1061,12 @@
"profile_addLink": "Link zu deiner Website hinzufügen",
"profile_info": "Andere Nutzer können dein Profil finden, indem sie auf deinen Avatar in der Benutzerliste eines Dokumentes klicken.",
"profile_friendRequestSent": "Freundschaftsanfrage gesendet...",
"profile_friend": "{0} ist mit dir befreundet"
"profile_friend": "{0} ist mit dir befreundet",
"notification_padShared": "",
"notification_fileShared": "",
"notification_folderShared": "",
"share_selectAll": "",
"share_filterFriend": "",
"share_linkFriends": "",
"share_withFriends": ""
}

@ -228,13 +228,8 @@
"profile_create": "Δημιουργήστε προφίλ",
"profile_description": "Περιγραφή",
"profile_fieldSaved": "Η καινούρια καταχώρηση αποθηκεύτηκε: {0}",
"profile_inviteButton": "Σύνδεση",
"profile_inviteButtonTitle": "Δημιουργήστε έναν σύνδεσμο για να προσκαλέσετε αυτόν το χρήστη να συνδεθεί μαζί σας.",
"profile_inviteExplanation": "Πατώντας <strong>OK</strong> θα δημιουργηθεί ένας σύνδεσμος προς μια ασφαλή συνεδρία επικοινωνίας όπου <em>μόνο ο/η {0} θα μπορεί να ανοίξει.</em><br><br>Ο σύνδεσμος θα αντιγραφεί στην προσωρινή μνήμη και μπορεί να διαμοιραστεί δημόσια.",
"profile_viewMyProfile": "Προβολή του προφίλ μου",
"userlist_addAsFriendTitle": "Προσθήκη του/της \"{0}\" ως επαφή",
"userlist_thisIsYou": "Αυτός είστε εσείς (\"{0}\")",
"userlist_pending": "Εκρεμμεί...",
"contacts_title": "Επαφές",
"contacts_addError": "Σφάλμα κατά την προσθήκη αυτής της επαφής στη λίστα",
"contacts_added": "Η επαφή αποδέχτηκε την πρόσκληση.",

@ -385,12 +385,8 @@
"updated_0_header_logoTitle": "Volver a tu CryptDrive",
"realtime_unrecoverableError": "El motor de tiempo real ha encontrado un error. Haga clic en OK para recargar la página.",
"typing": "Editando",
"profile_inviteButton": "Conectar",
"profile_inviteButtonTitle": "Crear un enlace de invitación para este usuario.",
"profile_inviteExplanation": "Hacer clic en <strong>OK</strong> creará un enlace de mensaje seguro que <em>sólo {0} podrá ver.</em><br><br>El enlace será copiado a tu portapapeles y puede ser compartido públicamente.",
"profile_viewMyProfile": "Ver mi perfil",
"userlist_addAsFriendTitle": "Agregar \"{0}\" como contacto",
"userlist_thisIsYou": "Tú mismo (\"{0}\")",
"userlist_addAsFriendTitle": "Enviar \"{0}\" una solicitud de amistad",
"contacts_title": "Contactos",
"contacts_addError": "Error al agregar este contacto a la lista",
"contacts_added": "Invitación aceptada",
@ -412,7 +408,6 @@
"settings_userFeedbackTitle": "Feedback",
"settings_logoutEverywhereButton": "Cerrar sesión",
"upload_title": "Subir archivo",
"userlist_pending": "Pendiente...",
"contacts_typeHere": "Escribe un mensaje aquí...",
"contacts_removeHistoryTitle": "Borrar el historial de chat",
"contacts_confirmRemoveHistory": "¿Estás seguro de que quieres borrar el historial de forma permanente? No se podrán recuparar los datos.",
@ -561,5 +556,44 @@
"fc_remove_sharedfolder": "Eliminar",
"fc_hashtag": "Etiquetas",
"register_passwordTooShort": "La contraseña debe tener por los menos {0} caracteres de largo.",
"useTemplateCancel": "Recomenzar (Esc)"
"useTemplateCancel": "Recomenzar (Esc)",
"register_whyRegister": "Por qué conectarse?",
"settings_cat_cursor": "Cursor",
"settings_cat_pad": "Texto enriquecido",
"settings_cat_creation": "Nueva hoja",
"settings_cat_subscription": "Suscripción",
"settings_backupHint": "Respalde o recupere todo sus contenido ne CryptDrive. No tendrán el contenido de sus archivos, solo las llaves para acceder a ellos.",
"settings_backupHint2": "Descargue el contenido actual de sus notas. Las notas serán descargadas en un formato de leíble si el formato esta disponible.",
"settings_backup2": "Descargar mi CryptDrive",
"settings_backup2Confirm": "Esto descargara todas las notas y archivos desde su CryptDrive. Si desea continuar, elija un nombre y presione OK",
"settings_exportTitle": "Exportar su CryptDrive",
"settings_exportDescription": "Por favor espere mientras descargamos y desencriptamos sus documentos. Esto podría tomar unos minutos. Al cerrar la etiqueta interrumpirá el proceso.",
"settings_exportFailed": "Si la nota requiere más de un minuto en ser descargada no será incluida en el archivo exportado. Un link para cada nota que no haya exportada será mostrada.",
"settings_exportWarning": "Nota: esta herramienta aún está en versión beta y podría tener problema al ser escalada. Para una mejor performance se recomiendo dejar esta etiqueta con foco.",
"settings_exportCancel": "Está seguro que quiera cancelar la exportación? Tendrá que comenzar desde el comienzo la próxima vez.",
"settings_export_reading": "Leyendo su CryptDrive...",
"settings_export_download": "Descargando y desencriptando sus documentos...",
"settings_export_compressing": "Comprimiendo...",
"settings_export_done": "Sus descarga está lista!",
"settings_exportError": "Ver errores",
"settings_exportErrorDescription": "No logramos agregar los siguientes documentos en lo exportado:",
"settings_exportErrorEmpty": "Este documento no puede ser exportado (contenido vacío o invalido).",
"settings_exportErrorMissing": "Este documento no puede ser encontrado en nuestros servidores (expirado o borrado por su dueño)",
"settings_exportErrorOther": "Ha ocurrido un error al tratar de exportar este documento: {0}",
"settings_thumbnails": "Imágenes en miniatura",
"settings_disableThumbnailsAction": "Deshabilitar la creación de imágenes pequeñas en su CryptDrive",
"settings_disableThumbnailsDescription": "Las imágenes en miniatura serán guardadas en su navegador de manera automática cuando visite una nueva hoja. Puede deshabilitar esta función acá.",
"settings_resetThumbnailsAction": "Limpiar",
"settings_resetThumbnailsDescription": "Limpiar todas las imágenes en miniatura de sus notas guardadas en su navegador.",
"settings_resetThumbnailsDone": "Todas las imágenes en miniatura han sido borradas.",
"settings_autostoreTitle": "Almacenamiento de notas en CryptDrive",
"settings_autostoreYes": "Automático",
"settings_autostoreNo": "Manual (jamás preguntar)",
"settings_autostoreMaybe": "Manual (preguntar siempre)",
"settings_deleteTitle": "Borrar cuenta",
"settings_deleteHint": "Borra la cuenta es permanente. SU CryptDrive y la lista de notas serán borradas del servidor. El resto de sus notas sera borrada en 90 días si nadie mas las ha guardado en su CryptDrive.",
"settings_deleteButton": "Borrar su cuenta",
"settings_deleteModal": "Compartir la siguiente información con el administrado de su CryptDrive a fin de que sus datos sean removidos de su servidor.",
"settings_deleteConfirm": "Presionar OK borrará su cuanta de manera permanente. Está seguro?",
"settings_deleted": "Tu cuenta de usuario ha sido borrada. Presione OK para ir a la página principal."
}

@ -277,13 +277,8 @@
"profile_create": "Créer un profil",
"profile_description": "Description",
"profile_fieldSaved": "Nouvelle valeur enregistrée : {0}",
"profile_inviteButton": "Inviter",
"profile_inviteButtonTitle": "Créer un lien pour inviter cet utilisateur à se connecter avec vous.",
"profile_inviteExplanation": "Cliquer sur <strong>OK</strong> créera un lien vers une session de messagerie sécurisée <em>uniquement accessible par {0}.</em><br><br>Le lien peut être copié et partagé de manière publique.",
"profile_viewMyProfile": "Voir mon profil",
"userlist_addAsFriendTitle": "Envoyer une demande d'ami à « {0} »",
"userlist_thisIsYou": "Vous (« {0} »)",
"userlist_pending": "En attente...",
"contacts_title": "Contacts",
"contacts_addError": "Erreur lors de l'ajout de ce contact dans votre liste",
"contacts_added": "Invitation de contact acceptée.",
@ -383,6 +378,8 @@
"fc_rename": "Renommer",
"fc_open": "Ouvrir",
"fc_open_ro": "Ouvrir (lecture seule)",
"fc_expandAll": "Développer tout",
"fc_collapseAll": "Réduire tout",
"fc_delete": "Déplacer vers la corbeille",
"fc_delete_owned": "Supprimer du serveur",
"fc_restore": "Restaurer",
@ -1066,5 +1063,16 @@
"profile_addLink": "Ajouter un lien vers votre site web",
"profile_info": "Les autres utilisateurs peuvent trouver votre profil en cliquant sur votre nom dans la liste d'utilisateurs des documents.",
"profile_friendRequestSent": "Demande d'ami en attente...",
"profile_friend": "{0} est votre ami(e)"
"profile_friend": "{0} est votre ami(e)",
"notification_padShared": "{0} a partagé un pad avec vous : <b>{1}</b>",
"notification_fileShared": "{0} a partagé un fichier avec vous : <b>{1}</b>",
"notification_folderShared": "{0} a partagé un dossier avec vous : <b>{1}</b>",
"share_selectAll": "Tout sélectionner",
"share_deselectAll": "Aucun",
"share_filterFriend": "Rechercher par nom",
"share_linkFriends": "Partager avec des amis",
"share_withFriends": "Partager",
"notifications_dismiss": "Cacher",
"fm_info_sharedFolderHistory": "Vous regardez l'historique de votre dossier partagé <b>{0}</b><br/>Votre CryptDrive restera en lecture seule pendant la navigation.",
"share_description": "Choisissez ce que vous souhaitez partager puis obtenez le lien ou envoyez-le directement à vos amis CryptPad."
}

@ -278,13 +278,8 @@
"profile_create": "Crea un profilo",
"profile_description": "Descrizione",
"profile_fieldSaved": "Nuovo valore salvato: {0}",
"profile_inviteButton": "Connetti",
"profile_inviteButtonTitle": "Crea un link per invitare questo utente a connettersi con te.",
"profile_inviteExplanation": "Clickando su <strong>OK</strong> verrà creato un link per una sessione di chat sicura che <em>solo {0} potrà visualizzare.</em><br><br>Il link verrà copiato nella tua clipboard e può essere condiviso pubblicamente.",
"profile_viewMyProfile": "Vedi il mio profilo",
"userlist_addAsFriendTitle": "Aggiungi \"{0}\" come contatto",
"userlist_thisIsYou": "Questo sei tu (\"{0}\")",
"userlist_pending": "In attesa...",
"contacts_title": "Contatti",
"contacts_addError": "Errore nell'inserimento di questo contatto nella lista",
"contacts_added": "Invito ai contatti accettato.",
@ -429,29 +424,29 @@
"register_whyRegister": "Perché registrarsi?",
"register_header": "Benvenuto su CryptPad",
"fm_alert_anonymous": "",
"register_writtenPassword": "",
"register_cancel": "",
"register_warning": "",
"register_alreadyRegistered": "",
"settings_cat_account": "",
"settings_cat_drive": "",
"settings_cat_cursor": "",
"settings_cat_code": "",
"settings_cat_pad": "",
"settings_cat_creation": "",
"settings_cat_subscription": "",
"settings_title": "",
"settings_save": "",
"settings_backupCategory": "",
"settings_backupHint": "",
"settings_backup": "",
"settings_restore": "",
"settings_backupHint2": "",
"settings_backup2": "",
"settings_backup2Confirm": "",
"settings_exportTitle": "",
"settings_exportDescription": "",
"settings_exportFailed": "",
"register_writtenPassword": "Ho scritto su carta il mio username e la mia password, procedi",
"register_cancel": "Torna indietro",
"register_warning": "Zero Knowledge significa che non possiamo recuperare i tuoi dati se perdi la tua password.",
"register_alreadyRegistered": "Questo utente esiste già, vuoi effettuare il log in?",
"settings_cat_account": "Account",
"settings_cat_drive": "CryptDrive",
"settings_cat_cursor": "Cursore",
"settings_cat_code": "Codice",
"settings_cat_pad": "Testo",
"settings_cat_creation": "Nuovo pad",
"settings_cat_subscription": "Sottoscrizione",
"settings_title": "Impostazioni",
"settings_save": "Salva",
"settings_backupCategory": "Backup",
"settings_backupHint": "Effettua il backup o importane uno con tutto il contenuto del tuo CryptDrive. Non conterrà il contenuto dei pads, solo le chiavi per potervi accedere.",
"settings_backup": "Backup",
"settings_restore": "Importa",
"settings_backupHint2": "Scarica il contenuto corrente dei tuoi pads. I pads saranno scaricati in un formato leggibile, se possibile.",
"settings_backup2": "Scarica il mio CryptDrive",
"settings_backup2Confirm": "Questo scaricherà tutti i tuoi pads e files dal tuo CryptDrive. Se vuoi continuare, scegli un nome e premi OK",
"settings_exportTitle": "Esporta il tuo CryptDrive",
"settings_exportDescription": "Per favore attendi mentre scarichiamo e decriptiamo i tuoi documenti. Potrebbe richiedere qualche minuto. Chiudere la finestra interromperà il processo.",
"settings_exportFailed": "Se il pad richiede più di un minuto per essere scaricato, non sarà incluso nell'export. Un link a qualsiasi pad non esportato sarà mostrato.",
"settings_exportWarning": "",
"settings_exportCancel": "",
"settings_export_reading": "",
@ -478,5 +473,15 @@
"settings_disableThumbnailsDescription": "",
"settings_resetThumbnailsAction": "",
"settings_resetThumbnailsDescription": "",
"settings_resetThumbnailsDone": ""
"settings_resetThumbnailsDone": "",
"settings_import": "Importa",
"settings_importConfirm": "Sei sicuro di voler importare i tuoi pads recenti dal browser sul tuo account CryptDrive?",
"settings_autostoreMaybe": "Manuale (chiedi sempre)",
"settings_userFeedbackTitle": "Feedbacks",
"settings_userFeedbackHint1": "CryptPad fornisce solo alcuni feedback basilari al server, per aiutarci a migliorare la tua esperienza. ",
"settings_userFeedbackHint2": "Il contenuto dei tuoi pads non sarà mai condiviso con il server.",
"settings_userFeedback": "Abilita feedback dell'user",
"settings_deleteTitle": "Cancella account",
"settings_deleteHint": "La cancellazione dell'account è permanente. Il tuo CryptDrive e la tua lista di pads sarà cancellata dal server. Il resto dei tuoi pads sarà cancellato in 90 giorni se nessun altro li ha salvati nel suo CryptDrive.",
"settings_deleteButton": "Cancella il tuo account"
}

@ -278,13 +278,8 @@
"profile_create": "Create a profile",
"profile_description": "Description",
"profile_fieldSaved": "New value saved: {0}",
"profile_inviteButton": "Connect",
"profile_inviteButtonTitle": "Create a link that will invite this user to connect with you.",
"profile_inviteExplanation": "Clicking <strong>OK</strong> will create a link to a secure messaging session that <em>only {0} will be able to redeem.</em><br><br>The link will be copied to your clipboard and can be shared publicly.",
"profile_viewMyProfile": "View my profile",
"userlist_addAsFriendTitle": "Send \"{0}\" a friend request",
"userlist_thisIsYou": "This is you (\"{0}\")",
"userlist_pending": "Pending...",
"contacts_title": "Contacts",
"contacts_addError": "Error while adding that contact to the list",
"contacts_added": "Contact invite accepted.",
@ -388,6 +383,8 @@
"fc_color": "Change color",
"fc_open": "Open",
"fc_open_ro": "Open (read-only)",
"fc_expandAll": "Expand All",
"fc_collapseAll": "Collapse All",
"fc_delete": "Move to trash",
"fc_delete_owned": "Delete from the server",
"fc_restore": "Restore",
@ -1067,5 +1064,16 @@
"profile_addLink": "Add a link to your website",
"profile_info": "Other users can find your profile through your avatar in document user lists.",
"profile_friendRequestSent": "Friend request pending...",
"profile_friend": "{0} is your friend"
"profile_friend": "{0} is your friend",
"notification_padShared": "{0} has shared a pad with you: <b>{1}</b>",
"notification_fileShared": "{0} has shared a file with you: <b>{1}</b>",
"notification_folderShared": "{0} has shared a folder with you: <b>{1}</b>",
"share_selectAll": "Select all",
"share_deselectAll": "Deselect all",
"share_filterFriend": "Search by name",
"share_linkFriends": "Share with friends",
"share_withFriends": "Share",
"notifications_dismiss": "Dismiss",
"fm_info_sharedFolderHistory": "This is only the history of your shared folder: <b>{0}</b><br/>Your CryptDrive will stay in read-only mode while you navigate.",
"share_description": "Choose what you'd like to share and either get the link or send it directly to your CryptPad friends."
}

@ -232,13 +232,8 @@
"profile_create": "Lag en ny profil",
"profile_description": "Beskrivelse",
"profile_fieldSaved": "Ny verdi er lagret:{0}",
"profile_inviteButton": "Koble til",
"profile_inviteButtonTitle": "Lag en invitasjonslink til denne brukeren.",
"profile_inviteExplanation": "Om du klikker <strong>OK</strong> vil vi lage en link til en sikker meldingsøkt som <em>bare {0} kan hente.</em><br><br>Linken kan deles fritt fra utklippstavla di.",
"profile_viewMyProfile": "Se profilen min",
"userlist_addAsFriendTitle": "Legg til \"{0}\" som ny kontakt",
"userlist_thisIsYou": "Dette er deg (\"{0}\")",
"userlist_pending": "Venter…",
"contacts_title": "Kontakter",
"contacts_addError": "Beklager feil, kontakten ble ikke lagt til i lista",
"contacts_added": "Kontaktinvitasjon er akseptert.",

@ -457,14 +457,9 @@
"profile_create": "Crează un profil",
"profile_description": "Descriere",
"profile_fieldSaved": "Valoare nouă salvată: {0}",
"profile_inviteButton": "Conectează-te",
"profile_inviteButtonTitle": "Crează un link prin care să inviți acest utilizator să se conecteze cu tine.",
"profile_inviteExplanation": "Dând click pe <strong>OK</strong> va crea un link către o sesiune de mesagerie securizată pe care <em>numai {0} o vor putea reda.</em><br><br>Linkul va fi copiat către memoria temporară locală și poate fi distribuit public.",
"userlist_addAsFriendTitle": "Adaugă \"{0}\" în lista de contacte",
"userlist_thisIsYou": "Acesta ești tu (\"{0}\")",
"canvas_currentBrush": "Culoarea curentă",
"profile_viewMyProfile": "Vizualizare profil",
"userlist_pending": "Așteaptă...",
"contacts_title": "Contacte",
"contacts_addError": "A apărut o eroare în timpul adăugării la lista de contacte",
"contacts_added": "Invitația din partea contactului acceptată",

@ -102,7 +102,7 @@
"forgetPrompt": "Нажав ОК, вы удалите документ в корзину. Уверены?",
"movedToTrash": "Документ был удалён в корзину.<br><a href=\"/drive/\">Доступ к диску</a>",
"shareButton": "Поделиться",
"shareSuccess": "Ссылка скопирована в буфер обмена",
"shareSuccess": "Ссылка скопирована в буфер обмена.",
"userListButton": "Список пользователей",
"chatButton": "Чат",
"userAccountButton": "Ваш профиль",
@ -256,10 +256,7 @@
"profile_create": "Создать профиль",
"profile_description": "Описание",
"profile_fieldSaved": "Сохранено новое значение: {0}",
"profile_inviteButton": "Соединено",
"profile_viewMyProfile": "Посмотреть мой профиль",
"userlist_thisIsYou": "Это Вы (\"{0}\")",
"userlist_pending": "Ожидание...",
"contacts_title": "Контакты",
"contacts_added": "Приглашение принято контактом",
"contacts_rejected": "Контакт не принял приглашение",
@ -327,7 +324,6 @@
"profile_uploadTypeError": "Ошибка: тип вашего аватара не разрешен. Допускаются следующие типы: {0}",
"profile_error": "Ошибка при создании вашего профиля: {0}",
"profile_register": "Для создания профиля необходимо зарегистрироваться!",
"profile_inviteButtonTitle": "Создайте ссылку, которая предложит этому пользователю подключиться к вам.",
"userlist_addAsFriendTitle": "Добавить \"{0}\" в качестве контакта",
"contacts_addError": "Ошибка при добавлении контакта в список",
"contacts_warning": "Все, что вы вводите здесь, является постоянным и доступно для всех существующих и будущих пользователей этого пэда. Будьте осторожны с конфиденциальной информацией!",
@ -370,5 +366,28 @@
"fm_padIsOwned": "Вы владелец этого пэда",
"fm_padIsOwnedOther": "Этот пэд принадлежит другому пользователю",
"fm_deletedPads": "Эти пэды больше не существуют на сервере, они были удалены с вашего CryptDrive: {0}",
"fm_tags_name": "Имя тэга"
"fm_tags_name": "Имя тэга",
"printCSS": "Пользовательские настройки вида (CSS)",
"viewEmbedTag": "Чтобы встроить данный документ вставьте iframe в нужную страницу. Вы можете настроить внешний вид используя CSS и HTML атрибуты. ",
"debug_getGraphText": "Это код DOT для генерации графика истории этого документа:",
"fm_ownedPadsName": "Собственный",
"fm_info_anonymous": "Вы не вошли в учетную запись, поэтому срок действия ваших пэдов истечет через 3 месяца (<a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">find out more</a>). Они хранятся в вашем браузере, поэтому очистка истории может привести к их исчезновению..<br><a href=\"/register/\">Sign up</a> or <a href=\"/login/\">Log in</a> to keep them alive.<br>",
"fm_backup_title": "Резервная ссылка",
"fm_burnThisDriveButton": "Удалить всю информацию, хранящуюся на CryptPad в браузере.",
"fm_tags_used": "Количество использований",
"fm_restoreDrive": "Восстановление прежнего состояния диска. Для достижения наилучших результатов не вносите изменения в диск, пока этот процесс не будет завершен.",
"fm_passwordProtected": "Этот документ защищен паролем",
"fc_newfolder": "Новая папка",
"fc_newsharedfolder": "Новая общая папка",
"fc_rename": "Переименовать",
"fc_open": "Открыть",
"fc_open_ro": "Отркыть (режим чтения)",
"fc_delete": "Переместить в корзину",
"fc_delete_owned": "Удалить с сервера",
"fc_restore": "Восстановить",
"fc_remove_sharedfolder": "Удалить",
"fc_empty": "Удалить корзину",
"fc_prop": "Свойства",
"fc_hashtag": "Теги",
"fc_sizeInKilobytes": "Размер в килобайтах"
}

@ -6,33 +6,7 @@
<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>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -6,33 +6,7 @@
<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>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -229,7 +229,7 @@ define([
};
var getGraph = function (chainpad, cb) {
var hashes = metadataMgr.getPrivateData().availableHashes;
var hashes = metadataMgr.getPrivateData().hashes;
var hash = hashes.editHash || hashes.viewHash;
var chan = Hash.hrefToHexChannelId('/drive/#'+hash);

@ -4,6 +4,7 @@
@import (reference) "../../customize/src/less2/include/limit-bar.less";
@import (reference) "../../customize/src/less2/include/tokenfield.less";
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/share.less';
&.cp-app-drive {
.framework_min_main(
@ -14,6 +15,7 @@
.limit-bar_main();
.tokenfield_main();
.share_main();
@drive_hover: #eee;
@drive_hover-light: lighten(@drive_hover, 20%);
@ -118,7 +120,7 @@
}
#cp-app-drive-tree {
resize: none;
width: 100%;
width: 100% !important;
max-width: unset;
max-height: unset;
border-bottom: 1px solid @drive_mobile-tree-border-col;
@ -770,6 +772,9 @@
.cp-toolbar-icon-history {
float: right;
&.active {
background-color: rgba(0, 0, 255, 0.2);
}
.cp-toolbar-drawer-element {
display: none;
}
@ -820,6 +825,7 @@
padding-left: 10px;
}
.cp-app-drive-toolbar-leftside {
flex-shrink: 0;
& > span {
height: 100%;
margin: 0;
@ -861,30 +867,58 @@
width: auto;
overflow: hidden;
white-space: nowrap;
display: flex;
flex-flow: row-reverse;
flex-shrink: 1;
min-width: 50px;
max-width: 100%;
text-align: left;
display: flex;
flex-direction: row;
.cp-app-drive-path-inner {
display: flex;
flex-flow: row-reverse;
flex-grow: 1;
.cp-app-drive-path-element {
display: inline-block;
flex-shrink: 0;
max-width: 100%;
height: @variables_bar-height;
line-height: @variables_bar-height;
font-size: @colortheme_app-font-size;
padding: 0 5px;
border: 0;
background: darken(@colortheme_drive-bg, 10%);
background: darken(@colortheme_drive-bg, 7%);
color: @colortheme_drive-color;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.15s;
&:first-child {
flex-shrink: 1;
}
&.cp-app-drive-path-separator {
color: #ccc;
}
&.cp-app-drive-path-lickable {
cursor: pointer;
&.cp-app-drive-path-collapse {
position: relative;
}
&:hover {
background: darken(@colortheme_drive-bg, 15%);
&:not(.cp-app-drive-path-separator) {
background-color: darken(@colortheme_drive-bg, 15%);
text-decoration: underline;
cursor: pointer;
}
& ~ .cp-app-drive-path-element {
background-color: darken(@colortheme_drive-bg, 15%);
}
& ~ .cp-app-drive-path-element:not(.cp-app-drive-path-separator) {
text-decoration: underline;
}
}
}
}

@ -6,33 +6,7 @@
<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>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -177,6 +177,31 @@ define([
APP.store[LS_OPENED] = JSON.stringify(stored);
localStore.put(LS_OPENED, JSON.stringify(stored));
};
var removeFoldersOpened = function (parentPath) {
var stored = JSON.parse(APP.store[LS_OPENED] || '[]');
var s = JSON.stringify(parentPath).slice(0, -1);
for (var i = stored.length - 1 ; i >= 0 ; i--) {
if (stored[i].indexOf(s) === 0) {
stored.splice(i, 1);
}
}
APP.store[LS_OPENED] = JSON.stringify(stored);
localStore.put(LS_OPENED, JSON.stringify(stored));
};
var renameFoldersOpened = function (parentPath, newName) {
var stored = JSON.parse(APP.store[LS_OPENED] || '[]');
var s = JSON.stringify(parentPath).slice(0, -1);
var newParentPath = parentPath.slice();
newParentPath[newParentPath.length - 1] = newName;
var sNew = JSON.stringify(newParentPath).slice(0, -1);
for (var i = 0 ; i < stored.length ; i++) {
if (stored[i].indexOf(s) === 0) {
stored[i] = stored[i].replace(s, sNew);
}
}
APP.store[LS_OPENED] = JSON.stringify(stored);
localStore.put(LS_OPENED, JSON.stringify(stored));
};
var getViewModeClass = function () {
var mode = APP.store[LS_VIEWMODE];
@ -271,6 +296,18 @@ define([
'tabindex': '-1',
'data-icon': faReadOnly,
}, Messages.fc_open_ro)),
h('li', h('a.cp-app-drive-context-expandall.dropdown-item', {
'tabindex': '-1',
'data-icon': "expandAll",
}, Messages.fc_expandAll)),
h('li', h('a.cp-app-drive-context-collapseall.dropdown-item', {
'tabindex': '-1',
'data-icon': "collapseAll",
}, Messages.fc_collapseAll)),
h('li', h('a.cp-app-drive-context-color.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faColor,
}, Messages.fc_color)),
h('li', h('a.cp-app-drive-context-download.dropdown-item', {
'tabindex': '-1',
'data-icon': faDownload,
@ -332,10 +369,6 @@ define([
'tabindex': '-1',
'data-icon': faRename,
}, Messages.fc_rename)),
h('li', h('a.cp-app-drive-context-color.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faColor,
}, Messages.fc_color)),
h('li', h('a.cp-app-drive-context-delete.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faTrash,
@ -781,7 +814,20 @@ define([
e.stopPropagation();
if (e.which === 13) {
removeInput(true);
manager.rename(path, $input.val(), refresh);
var newName = $input.val();
if (JSON.stringify(path) === JSON.stringify(currentPath)) {
manager.rename(path, $input.val(), function () {
renameFoldersOpened(path, newName);
path[path.length - 1] = newName;
APP.displayDirectory(path);
});
}
else {
manager.rename(path, $input.val(), function () {
renameFoldersOpened(path, newName);
refresh();
});
}
return;
}
if (e.which === 27) {
@ -877,6 +923,10 @@ define([
paths.forEach(function (p) {
var path = p.path;
var $element = p.element;
if (!$element.closest("#cp-app-drive-tree").length) {
hide.push('expandall');
hide.push('collapseall');
}
if (path.length === 1) {
// Can't rename or delete root elements
hide.push('delete');
@ -909,6 +959,8 @@ define([
if (containsFolder) {
// More than 1 folder selected: cannot create a new subfolder
hide.push('newfolder');
hide.push('expandall');
hide.push('collapseall');
}
containsFolder = true;
hide.push('openro');
@ -919,6 +971,8 @@ define([
if (containsFolder) {
// More than 1 folder selected: cannot create a new subfolder
hide.push('newfolder');
hide.push('expandall');
hide.push('collapseall');
}
containsFolder = true;
hide.push('openro');
@ -971,7 +1025,7 @@ define([
show = ['newfolder', 'newsharedfolder', 'newdoc'];
break;
case 'tree':
show = ['open', 'openro', 'download', 'share', 'rename', 'color', 'delete', 'deleteowned', 'removesf',
show = ['open', 'openro', 'expandall', 'collapseall', 'color', 'download', 'share', 'rename', 'delete', 'deleteowned', 'removesf',
'newfolder', 'properties', 'hashtag'];
break;
case 'default':
@ -1257,7 +1311,11 @@ define([
if (manager.isPathIn(newPath, [TRASH]) && paths.length && paths[0][0] === TRASH) {
return;
}
manager.move(paths, newPath, cb, copy);
var newCb = function () {
paths.forEach(removeFoldersOpened);
cb();
};
manager.move(paths, newPath, newCb, copy);
};
// Delete paths from the drive and/or shared folders (without moving them to the trash)
var deletePaths = function (paths, pathsList) {
@ -1284,7 +1342,10 @@ define([
UI.confirm(msg, function(res) {
$(window).focus();
if (!res) { return; }
manager.delete(pathsList, refresh);
manager.delete(pathsList, function () {
pathsList.forEach(removeFoldersOpened);
refresh();
});
}, null, true);
};
// Drag & drop:
@ -1672,6 +1733,63 @@ define([
return pName;
};
var drivePathOverflowing = function () {
var $container = $(".cp-app-drive-path");
if ($container.length) {
$container.css("overflow", "hidden");
var overflown = $container[0].scrollWidth > $container[0].clientWidth;
$container.css("overflow", "");
return overflown;
}
};
var collapseDrivePath = function () {
var $container = $(".cp-app-drive-path-inner");
var $spanCollapse = $(".cp-app-drive-path-collapse");
$spanCollapse.css("display", "none");
var $pathElements = $container.find(".cp-app-drive-path-element");
$pathElements.not($spanCollapse).css("display", "");
var oneFolder = currentPath.length > 1 + (currentPath[0] === SHARED_FOLDER);
if (oneFolder && drivePathOverflowing()) {
var collapseLevel = 0;
var removeOverflowElement = function () {
if (drivePathOverflowing()) {
if ($pathElements.length <= 3) {
return false;
}
collapseLevel++;
if ($($pathElements.get(-2)).is(".cp-app-drive-path-separator")) {
$($pathElements.get(-2)).css("display", "none");
$pathElements = $pathElements.not($pathElements.get(-2));
}
$($pathElements.get(-2)).css("display", "none");
$pathElements = $pathElements.not($pathElements.get(-2));
return true;
}
};
currentPath.every(removeOverflowElement);
$spanCollapse.css("display", "");
removeOverflowElement();
var tipPath = currentPath.slice(0, collapseLevel);
tipPath[0] = getPrettyName(tipPath[0]);
$spanCollapse.attr("title", tipPath.join(" / "));
$spanCollapse[0].onclick = function () {
APP.displayDirectory(getLastOpenedFolder().slice(0, collapseLevel));
};
}
};
window.addEventListener("resize", collapseDrivePath);
var treeResizeObserver = new MutationObserver(collapseDrivePath);
treeResizeObserver.observe($("#cp-app-drive-tree")[0], {"attributes": true});
var toolbarButtonAdditionObserver = new MutationObserver(collapseDrivePath);
$(function () { toolbarButtonAdditionObserver.observe($("#cp-app-drive-toolbar")[0], {"childList": true, "subtree": true}); });
// Create the title block with the "parent folder" button
var createTitle = function ($container, path, noStyle) {
if (!path || path.length === 0) { return; }
@ -1683,6 +1801,9 @@ define([
var el = isVirtual ? undefined : manager.find(path);
path = path[0] === SEARCH ? path.slice(0,1) : path;
var $inner = $('<div>', {'class': 'cp-app-drive-path-inner'});
$container.prepend($inner);
var skipNext = false; // When encountering a shared folder, skip a key in the path
path.forEach(function (p, idx) {
if (skipNext) { skipNext = false; return; }
@ -1715,13 +1836,21 @@ define([
var $span2 = $('<span>', {
'class': 'cp-app-drive-path-element cp-app-drive-path-separator'
}).text(' / ');
$container.prepend($span2);
$inner.prepend($span2);
}
$span.text(name).prependTo($container);
$span.text(name).prependTo($inner);
});
var $spanCollapse = $('<span>', {
'class': 'cp-app-drive-path-element cp-app-drive-path-collapse'
}).text(' ... ');
$inner.append($spanCollapse);
collapseDrivePath();
};
var createInfoBox = function (path) {
var $box = $('<div>', {'class': 'cp-app-drive-content-info-box'});
var msg;
@ -1749,6 +1878,12 @@ define([
default:
msg = undefined;
}
if (history.isHistoryMode && history.sfId) {
// Shared folder history: always display the warning
var sfName = (manager.getSharedFolderData(history.sfId) || {}).title || Messages.fm_sharedFolderName;
msg = Messages._getKey('fm_info_sharedFolderHistory', [sfName]);
return $(common.fixLinks($box.html(msg)));
}
if (!APP.loggedIn) {
msg = APP.newSharedFolder ? Messages.fm_info_sharedFolder : Messages.fm_info_anonymous;
return $(common.fixLinks($box.html(msg)));
@ -2672,25 +2807,29 @@ define([
// NOTE: Elements in the trash are not using the same storage structure as the others
var _displayDirectory = function (path, force) {
APP.hideMenu();
if (!APP.editable) { debug("Read-only mode"); }
if (!appStatus.isReady && !force) { return; }
if (!path || path.length === 0) {
// Only Trash and Root are available in not-owned files manager
if (!path || displayedCategories.indexOf(path[0]) === -1) {
log(Messages.fm_categoryError);
currentPath = [ROOT];
_displayDirectory(currentPath);
return;
}
if (!APP.loggedIn && APP.newSharedFolder) {
// ANON_SHARED_FOLDER
path = [SHARED_FOLDER, ROOT];
} else {
path = [ROOT];
}
}
appStatus.ready(false);
currentPath = path;
var s = $content.scrollTop() || 0;
$content.html("");
sel.$selectBox = $('<div>', {'class': 'cp-app-drive-content-select-box'})
.appendTo($content);
if (!path || path.length === 0) {
path = [ROOT];
}
var isInRoot = manager.isPathIn(path, [ROOT]);
var inTrash = manager.isPathIn(path, [TRASH]);
var isTrashRoot = manager.comparePath(path, [TRASH]);
@ -2701,6 +2840,10 @@ define([
var isTags = path[0] === TAGS;
// ANON_SHARED_FOLDER
var isSharedFolder = path[0] === SHARED_FOLDER && APP.newSharedFolder;
if (isSharedFolder && path.length < 2) {
path = [SHARED_FOLDER, 'root'];
currentPath = path;
}
var root = isVirtual ? undefined : manager.find(path);
if (manager.isSharedFolder(root)) {
@ -3195,6 +3338,7 @@ define([
//data.noPassword = true;
data.noEditPassword = true;
data.noExpiration = true;
data.sharedFolder = true; // XXX debug
}
UIElements.getProperties(common, data, cb);
@ -3214,7 +3358,10 @@ define([
UI.confirm(msgD, function(res) {
$(window).focus();
if (!res) { return; }
manager.delete(pathsList, refresh);
manager.delete(pathsList, function () {
pathsList.forEach(removeFoldersOpened);
refresh();
});
});
};
$contextMenu.on("click", "a", function(e) {
@ -3236,11 +3383,15 @@ define([
}
if ($(this).hasClass("cp-app-drive-context-color")) {
var currentColor = getFolderColor(paths[0].path);
var to;
pickFolderColor(paths[0].element, currentColor, function (color) {
paths.forEach(function (p) {
setFolderColor(p.element, p.path, color);
});
clearTimeout(to);
to = setTimeout(function () {
refresh(); // makes imgs overview flicker in drive
}, 300);
});
}
else if($(this).hasClass("cp-app-drive-context-delete")) {
@ -3278,6 +3429,32 @@ define([
openFile(null, href);
});
}
else if ($(this).hasClass('cp-app-drive-context-expandall') ||
$(this).hasClass('cp-app-drive-context-collapseall')) {
if (paths.length !== 1) { return; }
var opened = $(this).hasClass('cp-app-drive-context-expandall');
var openRecursive = function (path) {
setFolderOpened(path, opened);
var folderContent = manager.find(path);
var subfolders = [];
for (var k in folderContent) {
if (manager.isFolder(folderContent[k])) {
if (manager.isSharedFolder(folderContent[k])) {
subfolders.push([k].concat(manager.user.userObject.ROOT));
}
else {
subfolders.push(k);
}
}
}
subfolders.forEach(function (p) {
var subPath = path.concat(p);
openRecursive(subPath);
});
};
openRecursive(paths[0].path);
refresh();
}
else if ($(this).hasClass('cp-app-drive-context-download')) {
if (paths.length !== 1) { return; }
el = manager.find(paths[0].path);
@ -3292,12 +3469,17 @@ define([
if (paths.length !== 1) { return; }
el = manager.find(paths[0].path);
var parsed, modal;
var friends = common.getFriends();
if (manager.isSharedFolder(el)) {
data = manager.getSharedFolderData(el);
parsed = Hash.parsePadUrl(data.href);
modal = UIElements.createSFShareModal({
origin: APP.origin,
pathname: "/drive/",
friends: friends,
title: data.title,
common: common,
hashes: {
editHash: parsed.hash
}
@ -3310,6 +3492,7 @@ define([
var padData = {
origin: APP.origin,
pathname: "/" + padType + "/",
friends: friends,
hashes: {
editHash: parsed.hash,
viewHash: roParsed.hash,
@ -3319,13 +3502,16 @@ define([
hash: parsed.hash,
password: data.password
},
title: data.title,
common: common
};
modal = padType === 'file' ? UIElements.createFileShareModal(padData)
: UIElements.createShareModal(padData);
modal = UI.dialog.tabs(modal);
}
UI.openCustomModal(modal);
UI.openCustomModal(modal, {
wide: Object.keys(friends).length !== 0
});
}
else if ($(this).hasClass('cp-app-drive-context-newfolder')) {
if (paths.length !== 1) { return; }
@ -3581,7 +3767,32 @@ define([
return false;
});
APP.histConfig.onOpen = function () {
// If we're in a shared folder history, store its id in memory
// so that we remember that this isn't the drive history if
// we browse through the drive
var sfId = manager.isInSharedFolder(currentPath);
if (!sfId) {
delete history.sfId;
delete APP.histConfig.sharedFolder;
return;
}
history.sfId = sfId;
var data = manager.getSharedFolderData(sfId);
var parsed = Hash.parsePadUrl(data.href || data.roHref);
APP.histConfig.sharedFolder = {
hash: parsed.hash,
password: data.password
};
};
history.onEnterHistory = function (obj) {
if (history.sfId) {
if (!obj || typeof(obj) !== "object" || Object.keys(obj).length === 0) { return; }
copyObjectValue(folders[history.sfId], obj);
refresh();
return;
}
history.sfId = false;
copyObjectValue(files, obj.drive);
appStatus.isReady = true;
refresh();
@ -3721,13 +3932,22 @@ define([
APP.histConfig = {
onLocal: function () {
UI.addLoadingScreen({ loadingText: Messages.fm_restoreDrive });
var data = {};
if (history.sfId) {
copyObjectValue(folders[history.sfId], history.currentObj);
data.sfId = history.sfId;
data.drive = history.currentObj;
} else {
proxy.drive = history.currentObj.drive;
sframeChan.query("Q_DRIVE_RESTORE", history.currentObj.drive, function () {
data.drive = history.currentObj.drive;
}
sframeChan.query("Q_DRIVE_RESTORE", data, function () {
UI.removeLoadingScreen();
}, {
timeout: 5 * 60 * 1000
});
},
onOpen: function () {},
onRemote: function () {},
setHistory: setHistory,
applyVal: function (val) {

@ -6,25 +6,7 @@
<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>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -115,6 +115,14 @@ define([
var title = document.title = metadata.name;
Title.updateTitle(title || Title.defaultTitle);
var owners = metadata.owners;
if (owners) {
common.setPadAttribute('owners', owners);
}
if (metadata.type) {
common.setPadAttribute('fileType', metadata.type);
}
toolbar.addElement(['pageTitle'], {pageTitle: title});
toolbar.$rightside.append(common.createButton('forget', true));
toolbar.$rightside.append(common.createButton('properties', true));

@ -50,7 +50,8 @@ define([
var postMsg = function (data) {
iframe.postMessage(data, '*');
};
var whenReady = waitFor(function (msg) {
var w = waitFor();
var whenReady = function (msg) {
if (msg.source !== iframe) { return; }
var data = JSON.parse(msg.data);
if (!data.txid) { return; }
@ -67,7 +68,8 @@ define([
config.modules.SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) {
sframeChan = sfc;
}));
});
w();
};
window.addEventListener('message', whenReady);
}).nThen(function () {
var updateMeta = function () {

@ -7,27 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<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;
padding: 0px;
}
#sbox-iframe {
position: fixed;
top: 0px;
left: 0px;
bottom: 0px;
right: 0px;
width: 100%;
height: 100%;
border: none;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>

@ -6,33 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="/common/onlyoffice/main.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;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -6,33 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="/common/onlyoffice/main.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;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -6,33 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<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;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -6,33 +6,7 @@
<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>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -739,10 +739,6 @@ define([
var initThumbnails = function () {
var privateDat = metadataMgr.getPrivateData();
if (!privateDat.thumbnails) { return; } // Thumbnails are disabled
var hash = privateDat.availableHashes.editHash ||
privateDat.availableHashes.viewHash;
if (!hash) { return; }
var href = privateDat.pathname + '#' + hash;
var $el = $('.cp-app-poll-realtime');
//var $el = $('#cp-app-poll-table');
var scrollTop;
@ -771,7 +767,7 @@ define([
.css('position', '');
$('#cp-app-poll-form').scrollTop(scrollTop);
},
href: href,
type: 'poll',
getContent: function () { return JSON.stringify(APP.proxy.content); }
};
Thumb.initPadThumbnails(common, options);

@ -6,33 +6,7 @@
<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>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -93,7 +93,7 @@ define([
return;
}
var hash = common.getMetadataMgr().getPrivateData().availableHashes.viewHash;
var hash = common.getMetadataMgr().getPrivateData().hashes.viewHash;
var url = APP.origin + '/profile/#' + hash;
var $button = $('<button>', {

@ -6,33 +6,7 @@
<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>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -0,0 +1,8 @@
@import (reference) '../../customize/src/less2/include/colortheme-all.less';
@import (reference) '../../customize/src/less2/include/tippy.less';
@import (reference) '../../customize/src/less2/include/share.less';
&.cp-app-share {
.tippy_main();
.share_main();
}

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html style="height: 100%; background: transparent;">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/share/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
body #cp-loading {
display: none;
position: absolute;
top: 15vh;
bottom: 15vh;
left: 10vw;
right: 10vw;
z-index: 200000;
overflow: hidden;
}
body #cp-loading .cp-loading-container {
margin-top: 35vh;
}
body #cp-loading .cp-loading-cryptofist {
display: none;
}
</style>
</head>
<body class="cp-app-share" style="background: transparent;">
</body>
</html>

@ -0,0 +1,83 @@
define([
'jquery',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-ui-elements.js',
'/common/common-interface.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/share/app-share.less',
], function (
$,
nThen,
SFCommon,
UIElements,
UI)
{
var APP = window.APP = {};
var andThen = function (common) {
var metadataMgr = common.getMetadataMgr();
var sframeChan = common.getSframeChannel();
var hideShareDialog = function () {
sframeChan.event('EV_SHARE_CLOSE');
};
var createShareDialog = function (data) {
var priv = metadataMgr.getPrivateData();
var hashes = priv.hashes;
var origin = priv.origin;
var pathname = priv.pathname;
var f = (data && data.file) ? UIElements.createFileShareModal
: UIElements.createShareModal;
var friends = common.getFriends();
var modal = f({
origin: origin,
pathname: pathname,
hashes: hashes,
common: common,
title: data.title,
friends: friends,
onClose: function () {
hideShareDialog();
},
fileData: {
hash: hashes.fileHash,
password: priv.password
}
});
UI.findCancelButton().click();
UI.openCustomModal(UI.dialog.tabs(modal), {
wide: Object.keys(friends).length !== 0
});
};
sframeChan.on('EV_SHARE_REFRESH', function (data) {
createShareDialog(data);
});
};
var main = function () {
var common;
nThen(function (waitFor) {
$(waitFor(function () {
UI.removeLoadingScreen();
}));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (/*waitFor*/) {
var metadataMgr = common.getMetadataMgr();
if (metadataMgr.getMetadataLazy() !== 'uninitialized') {
andThen(common);
return;
}
metadataMgr.onChange(function () {
andThen(common);
});
});
};
main();
});

@ -0,0 +1,148 @@
// 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',
'jquery',
'/common/requireconfig.js',
], function (nThen, ApiConfig, $, RequireConfig) {
var requireConfig = RequireConfig();
var ready = false;
var create = function (config) {
// Loaded in load #2
var sframeChan;
nThen(function (waitFor) {
$(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
$('#sbox-share-iframe').attr('src',
ApiConfig.httpSafeOrigin + '/share/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*/) {
var Cryptpad = config.modules.Cryptpad;
var Utils = config.modules.Utils;
nThen(function (waitFor) {
// The inner iframe tries to get some data from us every ms (cache, store...).
// It will send a "READY" message and wait for our answer with the correct txid.
// First, we have to answer to this message, otherwise we're going to block
// sframe-boot.js. Then we can start the channel.
var msgEv = Utils.Util.mkEvent();
var iframe = $('#sbox-share-iframe')[0].contentWindow;
var postMsg = function (data) {
iframe.postMessage(data, '*');
};
var w = waitFor();
var whenReady = function (msg) {
if (msg.source !== iframe) { return; }
var data = JSON.parse(msg.data);
if (!data.txid) { return; }
// Remove the listener once we've received the READY message
window.removeEventListener('message', whenReady);
// Answer with the requested data
postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage() }));
// Then start the channel
window.addEventListener('message', function (msg) {
if (msg.source !== iframe) { return; }
msgEv.fire(msg);
});
config.modules.SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) {
sframeChan = sfc;
}));
w();
};
window.addEventListener('message', whenReady);
}).nThen(function () {
var updateMeta = function () {
//console.log('EV_METADATA_UPDATE');
var metaObj;
nThen(function (waitFor) {
Cryptpad.getMetadata(waitFor(function (err, n) {
if (err) { console.log(err); }
metaObj = n;
}));
}).nThen(function (/*waitFor*/) {
metaObj.doc = {};
var additionalPriv = {
accountName: Utils.LocalStore.getAccountName(),
origin: window.location.origin,
pathname: window.location.pathname,
feedbackAllowed: Utils.Feedback.state,
hashes: config.data.hashes,
password: config.data.password,
file: config.data.file,
};
for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }
sframeChan.event('EV_METADATA_UPDATE', metaObj);
});
};
Cryptpad.onMetadataChanged(updateMeta);
sframeChan.onReg('EV_METADATA_UPDATE', updateMeta);
config.addCommonRpc(sframeChan);
sframeChan.on('Q_GET_FILES_LIST', function (types, cb) {
Cryptpad.getSecureFilesList(types, function (err, data) {
cb({
error: err,
data: data
});
});
});
sframeChan.on('EV_SHARE_CLOSE', function () {
config.onClose();
});
sframeChan.on('EV_SHARE_ACTION', function (data) {
config.onShareAction(data);
});
sframeChan.onReady(function () {
if (ready === true) { return; }
if (typeof ready === "function") {
ready();
}
ready = true;
});
});
});
var refresh = function (data, cb) {
if (!ready) {
ready = function () {
refresh(data, cb);
};
return;
}
sframeChan.event('EV_SHARE_REFRESH', data);
cb();
};
return {
refresh: refresh
};
};
return {
create: create
};
});

@ -6,33 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="/common/onlyoffice/main.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;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -6,33 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<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;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -458,6 +458,7 @@ define([
mkHelpMenu(framework);
CodeMirror.mkIndentSettings(framework._.cpNfInner.metadataMgr);
CodeMirror.init(framework.localChange, framework._.title, framework._.toolbar);
CodeMirror.configureTheme(common);
framework.onContentUpdate(function (newContent) {

@ -6,33 +6,7 @@
<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>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -6,33 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<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;
padding: 0px;
}
#sbox-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#sbox-filePicker-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;
width:100%;
height: 100%;
border: 0;
}
</style>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -445,11 +445,7 @@ define([
var oldThumbnailState;
var privateDat = metadataMgr.getPrivateData();
if (!privateDat.thumbnails) { return; }
var hash = privateDat.availableHashes.editHash ||
privateDat.availableHashes.viewHash;
var href = privateDat.pathname + '#' + hash;
var mkThumbnail = function () {
if (!hash) { return; }
if (framework.getState() !== 'READY') { return; }
if (!framework._.cpNfInner.chainpad) { return; }
var content = framework._.cpNfInner.chainpad.getUserDoc();
@ -457,7 +453,7 @@ define([
var D = Thumb.getResizedDimensions($canvas[0], 'pad');
Thumb.fromCanvas($canvas[0], D, function (err, b64) {
oldThumbnailState = content;
Thumb.setPadThumbnail(framework._.sfCommon, href, privateDat.channel, b64);
Thumb.setPadThumbnail(framework._.sfCommon, 'whiteboard', privateDat.channel, b64);
});
};
window.setInterval(mkThumbnail, Thumb.UPDATE_INTERVAL);

Loading…
Cancel
Save