Merge branch 'staging' into responsiveContextmenu

pull/1/head
ClemDee 5 years ago
commit 6bcf9f13d3

@ -1,3 +1,111 @@
# Zebra release (v2.25.0)
## Goals
This release coincided with XWiki's yearly seminar, so our regular schedule was interrupted a bit. We spent the time we had working towards implementing components of "editable metadata", which will allow pad owners to add new owners or transfer ownership to friends, among other things.
Otherwise we wanted to deploy a built-in support system to improve our ability to debug issues as well as to make it easier for users to report problems. Along the way we did our best to improve usability and fix small annoying bugs.
As this is the last release in our 2.0 cycle, we're going to take some extra time to prepare some big features for our 3.0.0 release, which we expect to deploy on August 20th, 2019.
## Update notes
* We've updated some dependencies that are used to lint the CryptPad codebase to detect errors. Run `npm install` if you plan to develop for CryptPad and you want to use the linter
* This release introduces a _support_ tab within the admin panel. If you generate an asymmetric keypair and add it to your server-side configuration file then users will have the option of opening support tickets if they encounter errors. Their support tickets will include some basic information about their account which might help you to solve their issues. To set up your _"encrypted support mailbox"_:
1. run `node ./scripts/generate-admin-keys.js`
2. copy the "public key" and add it to your config.js file like so:
* `supportMailboxPublicKey: "BL3kgYBM0HNw5ms8ULWU1wMTb5ePBbxAPjDZKamkuB8=",
3. copy the private key and store it in a safe place
4. navigate to the "support" tab in the admin panel and enter the private key
5. share the private key with any other administrators who should be able to read the support tickets
6. restart so that your users receive the public key stored in your configuration file
* this will allow them to submit tickets via the support page
* if you don't know how to fix the issue and want to open a ticket on our public tracker, include the information submitted along with their ticket
## Features
* The feature added in the previous release which displayed a preview of the theme and highlighting mode chosen for the code and slide editors has been improved to also display previews when navigating through the dropdowns using keyboard arrow keys.
* We've followed up on our initial work on notifications by adding a full notifications page which offers the ability to review older notifications that you might have accidentally dismissed.
* When you right-click on an element in the CryptDrive the resulting menu now includes icons to make it easier to find the action for which you are looking
* We now include folders in search results which used to only include files
* You can right-click to add colors to folders, in case that helps you organize your content more effectively
# Yak release (v2.24.0)
## Goals
We've recently had an intern join our team, so this release and those until the end of summer are likely to feature a lot of small usability fixes.
Otherwise, we've continued to develop team-centric features, particularly the way that registered users share pads with friends.
Finally, we prioritized the ability to archive files for a period instead of deleting them, which we've been planning for a while.
## Update notes
* There are some important steps in this release:
* **make sure you read the full update notes before proceeding!**
* [@zimbatm](https://github.com/zimbatm) added the ability to configure the location of your configuration file via environment variables when launching the server:
* `CRYPTPAD_CONFIG=/home/cryptpad/cryptpad/cryptpad-config/config.js /home/cryptpad/cryptpad/server.js`
* We discovered a bug in our Xenops release which resulted in the server's list of pads stored for each user to be incorrect.
* if you're running CryptPad 2.23.0, we recommend that you disable any scripts configured to delete inactive pads
* updating to 2.24.0 will fix the issue in the client, but each user's list of "pinned pads" won't be corrected until they visit your instance and run the latest code
* This release introduces the ability to archive some data instead of deleting it, since it can be scary to remove user data when you can't easily inspect it to see what it is
* to take advantage of this new functionality you'll need to update your configuration file with three new configuration points:
* set `retainData` to `true` if you want to archive channels instead of deleting them
* either by user command or due to inactivity
* the server will fall back to its default deletion behaviour if this value is `false` or not set at all
* set `archiveRetentionTime` to the number of days that an archived pad should be stored in the archive directory before being deleted permanently
* set `archivePath` to the path where you'd like archives to be stored
* it should not be publicly accessible in order to respect the users' wishes
* We've introduced some new scripts to work with the database, some of which were needed to diagnose problems stemming from the pinning bug
* `evict-inactive.js` identifies channels which are unpinned and inactive and archives them
* unlike `delete-inactive.js` it only handles channels, not files or any other kind of data
* ...but it's much safer, since nothing is removed permanently
* in the coming releases we'll implement archival for other types of data so that we can fully remove unsafe scripts
* `diagnose-archive-conflicts.js` checks all the files in your archive and identifies whether they can be restored safely or if they conflict with newer files in the production database
* `restore-archived.js` restores any channels archived by the server or evict-inactive.js, excluding those which would conflict with the database
* This release depends on updates to some serverside dependencies. Run `npm update`:
* `ws` addresses a potential vulnerability, so if possible anyone running earlier versions of CryptPad should update
* `chainpad-server` handles users' websocket connections and we needed to make a few changes to deal with changes in the `ws` API
* `heapdump` is no longer a default dependency, though you can install it if you want its functionality
* This release also features a **Clientside migration** which modifies users' CryptDrives. Any clients which are running both the latest code after the update as well as an older version in another browser or device risk creating conflicts in their account data. To prevent this, update in the following manner:
1. ensure that you've added the configuration values listed above
2. shut down the server and ensure that it doesn't restart until you've completed the following steps
3. pull the latest clientside and serverside code via git
4. `npm update` to get the latest serverside dependencies
5. update the cache-busting string if you are handling the cache manually, otherwise allow the server to handle this as per its default
5. restart the server: clients with open tabs should be prompted to reload instead of reconnecting because the server's version has changed
* We recommend that you test a local version of CryptPad before deploying this latest code, as aspects of the above-mentioned migrations are not backwards-compatible.
* you can roll back, but users' CryptDrives might have errors coping with data introduced by newer features.
## Features
* As mentioned above, CryptPad instances can be configured to temporarily archive files instead of deleting them permanently.
* as a user this means if you accidentally delete a file you have the option of contacting your administrator and asking them to help
* if they're really nice and have the spare time to help you, they might actually recover your data!
* A contributor is working on translating CryptPad into the Catalan language.
* if your preferred language isn't supported, you can do the same on https://weblate.cryptpad.fr
* We added the ability to add colors to folders in users CryptDrives, along with support for arbitrary folder metadata which we aren't using yet.
* Users with existing friends on the platform will run a migration to allow them to share pads with friends directly instead of sending them a link.
* they'll receive a notification indicating the title of the pad and who shared it
* if you've already added friends on the platform, you can send them pads from the usual "sharing menu"
* Our code editor already offered the ability to set their color theme and highlighting mode, but now those values will be previewed when mousing over the the option in the dropdown.
* Our slide editor now offers the same theme selection as the code editor
* It's now possible to view the history of a shared folder by clicking the history button while viewing the shared folder's contents.
## Bug fixes
* The CryptDrive received a number of usability fixes this time around:
* better styles when hovering over interactive elements in the drive (cursors, shading, etc)
* clicking the history button in the drive a second time will exit history mode
* after being resized, the tree pane now correctly responds to mobile layout styles
* the path indicator also adapts to very narrow layouts
* the user's current location is preserved when renaming the current folder or its ancestors
* you can right-click on elements in the tree and expand or collapse all of their children
* A user noticed that one-on-one chats did not seem to be deleted, as their messages were still available after a reload.
* they were deleted but our usage of the sharedWorker API incorrectly preserved a local cache of those message until you closed all of your browser tabs
* We've also fixed some elements of the chat UI, notably the position of the chat's scrollbar when first loading older messages and how the interface scrolls to keep up with new messages.
* We've noticed some cases of tooltips getting stuck in the UI and implemented some measures to prevent this from happening.
* After "unfriending" another user it was possible that they would be automatically re-added as friends.
# Xenops release (v2.23.0) # Xenops release (v2.23.0)
## Goals ## Goals

@ -33,6 +33,7 @@ VOLUME /cryptpad/tasks
VOLUME /cryptpad/block VOLUME /cryptpad/block
VOLUME /cryptpad/blob VOLUME /cryptpad/blob
VOLUME /cryptpad/blobstage VOLUME /cryptpad/blobstage
VOLUME /cryptpad/data
# Copy cryptpad and tini from the build container # Copy cryptpad and tini from the build container
COPY --from=build /sbin/tini /sbin/tini COPY --from=build /sbin/tini /sbin/tini
@ -44,4 +45,4 @@ WORKDIR /cryptpad
EXPOSE 3000 3001 EXPOSE 3000 3001
# Run cryptpad on startup # Run cryptpad on startup
CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"] CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"]

@ -64,6 +64,19 @@ module.exports = {
//"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=", //"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=",
], ],
/* CryptPad's administration panel includes a "support" tab
* wherein administrators with a secret key can view messages
* sent from users via the encrypted forms on the /support/ page
*
* To enable this functionality:
* run `node ./scripts/generate-admin-keys.js`
* save the public key in your config in the value below
* add the private key via the admin panel
* and back it up in a secure manner
*
*/
// supportMailboxPublicKey: "",
/* ===================== /* =====================
* Infra setup * Infra setup
* ===================== */ * ===================== */

@ -25,4 +25,5 @@
<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="&#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="new-template" d="M840.764 886.152h-655.119c-35.33 0-63.97-28.64-63.97-63.97v0-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.97v0zM844.499 34.545c0-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.735h655.119c2.063 0 3.735-1.672 3.735-3.735v0zM643.915 466.071h-93.365v93.003c0 10.313-8.36 18.673-18.673 18.673v0h-37.346c-0.036 0-0.078 0-0.121 0-10.246 0-18.552-8.306-18.552-18.552 0-0.042 0-0.085 0-0.127v0.006-93.003h-93.365c-0.036 0-0.078 0-0.121 0-10.246 0-18.552-8.306-18.552-18.552 0-0.042 0-0.085 0-0.127v0.006-37.346c0-10.313 8.36-18.673 18.673-18.673v0h93.365v-93.967c0-0.036 0-0.078 0-0.121 0-10.246 8.306-18.552 18.552-18.552 0.042 0 0.085 0 0.127 0h37.34c10.313 0 18.673 8.36 18.673 18.673v0 93.606h93.365c10.285 0.068 18.605 8.388 18.673 18.666v37.352c0.002 0.108 0.003 0.234 0.003 0.361 0 10.313-8.36 18.673-18.673 18.673-0.001 0-0.002 0-0.004 0v0z" /> <glyph unicode="&#xe910;" glyph-name="new-template" d="M840.764 886.152h-655.119c-35.33 0-63.97-28.64-63.97-63.97v0-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.97v0zM844.499 34.545c0-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.735h655.119c2.063 0 3.735-1.672 3.735-3.735v0zM643.915 466.071h-93.365v93.003c0 10.313-8.36 18.673-18.673 18.673v0h-37.346c-0.036 0-0.078 0-0.121 0-10.246 0-18.552-8.306-18.552-18.552 0-0.042 0-0.085 0-0.127v0.006-93.003h-93.365c-0.036 0-0.078 0-0.121 0-10.246 0-18.552-8.306-18.552-18.552 0-0.042 0-0.085 0-0.127v0.006-37.346c0-10.313 8.36-18.673 18.673-18.673v0h93.365v-93.967c0-0.036 0-0.078 0-0.121 0-10.246 8.306-18.552 18.552-18.552 0.042 0 0.085 0 0.127 0h37.34c10.313 0 18.673 8.36 18.673 18.673v0 93.606h93.365c10.285 0.068 18.605 8.388 18.673 18.666v37.352c0.002 0.108 0.003 0.234 0.003 0.361 0 10.313-8.36 18.673-18.673 18.673-0.001 0-0.002 0-0.004 0v0z" />
<glyph unicode="&#xe911;" 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" /> <glyph unicode="&#xe911;" 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" />
<glyph unicode="&#xe912;" glyph-name="folder-upload" d="M829.44 727.251h-296.84l-47.104 62.886c-25.923 34.066-66.406 55.898-111.999 56.139h-178.938c-77.457-0.137-140.211-62.891-140.348-140.335v-515.868c0.137-77.457 62.891-140.211 140.335-140.348h634.893c77.457 0.137 140.211 62.891 140.348 140.335v396.482c0 0.036 0 0.078 0 0.121 0 77.561-62.807 140.452-140.335 140.589h-0.013zM911.119 190.072c-0.068-45.083-36.597-81.611-81.673-81.679h-634.887c-45.083 0.068-81.611 36.597-81.679 81.673v515.862c0.068 45.083 36.597 81.611 81.673 81.679h178.906c26.48-0.030 50.004-12.656 64.908-32.207l0.146-0.199 47.104-62.765 17.709-24.094h326.114c45.125-0.069 81.679-36.665 81.679-81.799 0 0 0 0 0 0v0zM562.838 166.883h-102.039v203.957h-72.523l123.723 214.076 123.723-214.076h-72.885v-203.957z" />
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

@ -1,9 +1,9 @@
@font-face { @font-face {
font-family: 'cptools'; font-family: 'cptools';
src: src:
url('fonts/cptools.ttf?yr9e7c') format('truetype'), url('fonts/cptools.ttf?cljhos') format('truetype'),
url('fonts/cptools.woff?yr9e7c') format('woff'), url('fonts/cptools.woff?cljhos') format('woff'),
url('fonts/cptools.svg?yr9e7c#cptools') format('svg'); url('fonts/cptools.svg?cljhos#cptools') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -24,6 +24,9 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.cptools-folder-upload:before {
content: "\e912";
}
.cptools-folder-no-color:before { .cptools-folder-no-color:before {
content: "\e900"; content: "\e900";
} }

@ -169,6 +169,28 @@ define([], function () {
height: 100%; height: 100%;
background: #5cb85c; background: #5cb85c;
} }
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(1800deg);
}
}
.cp-spinner {
display: inline-block;
box-sizing: border-box;
width: 80px;
height: 80px;
border: 11px solid lightgrey;
border-radius: 50%;
border-top-color: transparent;
animation: spin infinite 3s;
animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85);
}
*/}).toString().slice(14, -3); */}).toString().slice(14, -3);
var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; }); var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; });
var elem = document.createElement('div'); var elem = document.createElement('div');
@ -182,7 +204,7 @@ define([], function () {
'</div>', '</div>',
'<div class="cp-loading-container">', '<div class="cp-loading-container">',
'<div class="cp-loading-spinner-container">', '<div class="cp-loading-spinner-container">',
'<span class="fa fa-spinner fa-pulse fa-4x fa-fw"></span>', '<span class="cp-spinner"></span>',
'</div>', '</div>',
'<p id="cp-loading-message"></p>', '<p id="cp-loading-message"></p>',
'</div>' '</div>'

@ -3,8 +3,8 @@
var map = { var map = {
'ca': 'Català', 'ca': 'Català',
'de': 'Deutsch', 'de': 'Deutsch',
'es': 'Español',
'el': 'Ελληνικά', 'el': 'Ελληνικά',
'es': 'Español',
'fr': 'Français', 'fr': 'Français',
'it': 'Italiano', 'it': 'Italiano',
'nb': 'Norwegian Bokmål', 'nb': 'Norwegian Bokmål',
@ -12,8 +12,8 @@ var map = {
'pt-br': 'Português do Brasil', 'pt-br': 'Português do Brasil',
'ro': 'Română', 'ro': 'Română',
'ru': 'Русский', 'ru': 'Русский',
//'te': 'తెలుగు',
'zh': '繁體中文', 'zh': '繁體中文',
'te': 'తెలుగు',
}; };
var messages = {}; var messages = {};

@ -103,7 +103,7 @@ define([
])*/ ])*/
]) ])
]), ]),
h('div.cp-version-footer', "CryptPad v2.23.0 (Xenops)") h('div.cp-version-footer', "CryptPad v2.25.0 (Zebra)")
]); ]);
}; };

@ -21,22 +21,6 @@ define([
h('h2.text-center', Msg.about_core) h('h2.text-center', Msg.about_core)
]), ]),
]), ]),
h('div.row.align-items-center', [
h('div.col-12.col-sm-12.col-md-12.col-lg-6.cp-bio-avatar', [
h('img.img-fluid', {'src': '/customize/images/CalebJames.jpg'})
]),
h('div.col-12.col-sm-12.col-md-12.col-lg-6.cp-profile-det', [
h('h3', "Caleb James Delisle"),
h('hr'),
Pages.setHTML(h('div#bioCaleb'), '<p>Caleb is a cryptography developer, Machine Technology graduate of the Franklin County Technical School and lifelong tinkerer.<br/>In 2011, he started the cjdns Open Source project to show that secure networking could be invisible and easily deployed.<br/>After joining XWiki SAS in 2014, he started the CryptPad project with the intent of bringing the same transparent security to collaborative editing.<br/>He\'s always trying to learn from more experienced colleagues and when someone passes through the Research Team office, his favorite words are "Pull up a chair!".</p>'),
h('a.cp-soc-media', { href : 'https://twitter.com/cjdelisle'}, [
h('i.fa.fa-twitter')
]),
h('a.cp-soc-media', { href : 'https://github.com/cjdelisle'}, [
h('i.fa.fa-github')
])
]),
]),
h('div.row.align-items-center',[ h('div.row.align-items-center',[
h('div.col-12.col-sm-12.col-md-12.col-lg-6.order-lg-2.cp-bio-avatar.cp-bio-avatar-right', [ h('div.col-12.col-sm-12.col-md-12.col-lg-6.order-lg-2.cp-bio-avatar.cp-bio-avatar-right', [
h('img.img-fluid', {'src': '/customize/images/AaronMacSween.jpg'}) h('img.img-fluid', {'src': '/customize/images/AaronMacSween.jpg'})
@ -74,16 +58,16 @@ define([
]), ]),
h('div.row.align-items-center', [ h('div.row.align-items-center', [
h('div.col-12.col-sm-12.col-md-12.col-lg-6.cp-bio-avatar', [ h('div.col-12.col-sm-12.col-md-12.col-lg-6.cp-bio-avatar', [
h('img.img-fluid', {'src': '/customize/images/Pierre-new.jpg'}) h('img.img-fluid', {'src': '/customize/images/CalebJames.jpg'})
]), ]),
h('div.col-12.col-sm-12.col-md-12.col-lg-6.cp-profile-det', [ h('div.col-12.col-sm-12.col-md-12.col-lg-6.cp-profile-det', [
h('h3', "Pierre Bondoerffer"), h('h3', "Caleb James Delisle"),
h('hr'), h('hr'),
Pages.setHTML(h('div#bioPierre'), '<p>Resident CSS wizard and emoji extraordinaire, Pierre is passionate about anything related to technology. He loves to hack around computers and put parts together.<br/>He is currently studying at 42, where he learns about algorithms, networking, kernel programming and graphics.<br/>As a part of an internship, he joined XWiki SAS and worked on CryptPad to improve user experience. He also maintains the Spanish translation.</p>'), Pages.setHTML(h('div#bioCaleb'), '<p>Caleb is a cryptography developer, Machine Technology graduate of the Franklin County Technical School and lifelong tinkerer.<br/>In 2011, he started the cjdns Open Source project to show that secure networking could be invisible and easily deployed.<br/>After joining XWiki SAS in 2014, he started the CryptPad project with the intent of bringing the same transparent security to collaborative editing.<br/>He\'s always trying to learn from more experienced colleagues and when someone passes through the Research Team office, his favorite words are "Pull up a chair!".</p>'),
h('a.cp-soc-media', { href : 'https://twitter.com/pbondoer'}, [ h('a.cp-soc-media', { href : 'https://twitter.com/cjdelisle'}, [
h('i.fa.fa-twitter') h('i.fa.fa-twitter')
]), ]),
h('a.cp-soc-media', { href : 'https://github.com/pbondoer'}, [ h('a.cp-soc-media', { href : 'https://github.com/cjdelisle'}, [
h('i.fa.fa-github') h('i.fa.fa-github')
]) ])
]), ]),

@ -137,6 +137,15 @@
@colortheme_admin-color: #FFF; @colortheme_admin-color: #FFF;
@colortheme_admin-warn: #ffae00; @colortheme_admin-warn: #ffae00;
@colortheme_notifications-bg: #4ae397;
@colortheme_notifications-color: #000;
@colortheme_notifications-warn: #e34a85;
@colortheme_support-bg: #42d1f4;
@colortheme_support-color: #000;
@colortheme_support-warn: #9A37F7;
// Sidebar layout (profile / settings) // Sidebar layout (profile / settings)
@colortheme_sidebar-active: #fff; @colortheme_sidebar-active: #fff;
@colortheme_sidebar-left-bg: #eee; @colortheme_sidebar-left-bg: #eee;

@ -23,7 +23,7 @@
} }
.dropdown-toggle { .dropdown-toggle {
transform: rotate(270deg); transform: rotate(270deg);
margin-left: 10px; margin-left: 1rem;
float: right; float: right;
} }
.dropdown-menu { .dropdown-menu {
@ -40,6 +40,7 @@
.fa, .cptools { .fa, .cptools {
margin-right: 1rem; margin-right: 1rem;
color: @colortheme_context-menu-icon-color; color: @colortheme_context-menu-icon-color;
width: 16px;
} }
} }
} }

@ -1,4 +1,5 @@
@import (reference) "./colortheme-all.less"; @import (reference) "./colortheme-all.less";
@import (reference) "./avatar.less";
.notifications_main() { .notifications_main() {
--LessLoader_require: LessLoader_currentFile(); --LessLoader_require: LessLoader_currentFile();
@ -14,6 +15,7 @@
display: flex; display: flex;
.cp-notification-content { .cp-notification-content {
flex: 1; flex: 1;
align-items: stretch;
min-width: 0; min-width: 0;
p { p {
word-break: break-word; word-break: break-word;
@ -28,8 +30,7 @@
.cp-notification-dismiss { .cp-notification-dismiss {
color: black; color: black;
width: 25px; width: 25px;
height: 100%; display: flex;
display: none;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
@ -39,6 +40,33 @@
} }
} }
} }
hr {
margin: 0px !important;
}
.cp-notifications-gotoapp {
p {
padding: 10px 0 !important;
text-align: center !important;
font-weight: bold;
cursor: pointer;
&:hover {
background-color: rgba(0,0,0,0.1);
}
}
}
.cp-notifications-requestedit-verified {
display: flex;
align-items: center;
&> span.cp-avatar {
.avatar_main(30px);
}
&> span {
margin-right: 10px;
}
&> p {
margin: 0;
}
}
} }

@ -90,12 +90,21 @@
button.btn { button.btn {
@button-bg: @colortheme_sidebar-button-bg; @button-bg: @colortheme_sidebar-button-bg;
@button-red-bg: @colortheme_sidebar-button-red-bg; @button-red-bg: @colortheme_sidebar-button-red-bg;
@button-alt-bg: @colortheme_sidebar-button-alt-bg;
background-color: @button-bg; background-color: @button-bg;
border-color: darken(@button-bg, 10%); border-color: darken(@button-bg, 10%);
color: white; color: white;
&:hover { &:hover {
background-color: darken(@button-bg, 10%); background-color: darken(@button-bg, 10%);
} }
&.btn-secondary {
background-color: @button-alt-bg;
border-color: darken(@button-alt-bg, 10%);
color: black;
&:hover {
background-color: darken(@button-alt-bg, 10%);
}
}
&.btn-danger { &.btn-danger {
background-color: @button-red-bg; background-color: @button-red-bg;
border-color: darken(@button-red-bg, 10%); border-color: darken(@button-red-bg, 10%);

@ -0,0 +1,86 @@
@import (reference) "./colortheme-all.less";
.support_main () {
@ticket-bg: #F7F7F7;
@msg-bg: #eee;
@fromme-bg: #ddd;
.cp-support-form-container {
[type="text"] {
width: @sidebar_button-width;
margin-bottom: 10px;
}
textarea {
width: 2*@sidebar_button-width;
max-width: 90%;
padding: 10px 15px;
height: 300px;
}
}
.cp-support-container {
.cp-support-list-ticket {
display: flex;
flex-flow: column;
background-color: @ticket-bg;
padding: 10px;
width: 1200px;
max-width: 90%;
margin: 5px auto;
.cp-support-list-message {
background-color: @msg-bg;
margin: 2px;
padding: 2px 5px;
.cp-support-fromme {
background-color: @fromme-bg;
}
.cp-support-showdata {
cursor: pointer;
background-color: @fromme-bg;
.cp-support-message-data {
display: none;
cursor: default;
}
}
.cp-support-message-time {
float: right;
}
pre {
margin-bottom: 0;
white-space: pre-wrap;
&.cp-support-message-content {
margin-top: 10px;
margin-bottom: 10px;
}
}
}
.cp-support-list-actions {
order: 3;
.cp-support-hide {
display: none;
}
}
.cp-support-form-container {
order: 2;
}
&.cp-support-list-closed {
.cp-support-list-actions {
display: block !important;
.cp-support-answer, .cp-support-close {
display: none;
}
.cp-support-hide {
display: inline;
}
}
.cp-support-form-container {
display: none !important;
}
}
button {
margin-left: 2px;
margin-right: 5px;
}
}
}
}

@ -31,3 +31,4 @@ services:
- ./data/tasks:/cryptpad/tasks:rw - ./data/tasks:/cryptpad/tasks:rw
- ./data/block:/cryptpad/block:rw - ./data/block:/cryptpad/block:rw
- ./data/config:/cryptpad/cfg:rw - ./data/config:/cryptpad/cfg:rw
- ./data/data:/cryptpad/data:rw

23
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"version": "2.23.0", "version": "2.25.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -573,16 +573,16 @@
"dev": true "dev": true
}, },
"jshint": { "jshint": {
"version": "2.9.7", "version": "2.10.2",
"resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.7.tgz", "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.2.tgz",
"integrity": "sha512-Q8XN38hGsVQhdlM+4gd1Xl7OB1VieSuCJf+fEJjpo59JH99bVJhXRXAh26qQ15wfdd1VPMuDWNeSWoNl53T4YA==", "integrity": "sha512-e7KZgCSXMJxznE/4WULzybCMNXNAd/bf5TSrvVEq78Q/K8ZwFpmBqQeDtNiHc3l49nV4E/+YeHU/JZjSUIrLAA==",
"dev": true, "dev": true,
"requires": { "requires": {
"cli": "~1.0.0", "cli": "~1.0.0",
"console-browserify": "1.1.x", "console-browserify": "1.1.x",
"exit": "0.1.x", "exit": "0.1.x",
"htmlparser2": "3.8.x", "htmlparser2": "3.8.x",
"lodash": "~4.17.10", "lodash": "~4.17.11",
"minimatch": "~3.0.2", "minimatch": "~3.0.2",
"shelljs": "0.3.x", "shelljs": "0.3.x",
"strip-json-comments": "1.0.x" "strip-json-comments": "1.0.x"
@ -697,9 +697,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.11", "version": "4.17.14",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==",
"dev": true "dev": true
}, },
"lodash.clonedeep": { "lodash.clonedeep": {
@ -709,10 +709,9 @@
"dev": true "dev": true
}, },
"lodash.merge": { "lodash.merge": {
"version": "4.6.1", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
"dev": true
}, },
"lodash.sortby": { "lodash.sortby": {
"version": "4.7.0", "version": "4.7.0",

@ -1,7 +1,7 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server", "description": "realtime collaborative visual editor with zero knowlege server",
"version": "2.23.0", "version": "2.25.0",
"license": "AGPL-3.0+", "license": "AGPL-3.0+",
"repository": { "repository": {
"type": "git", "type": "git",
@ -23,7 +23,7 @@
}, },
"devDependencies": { "devDependencies": {
"flow-bin": "^0.59.0", "flow-bin": "^0.59.0",
"jshint": "~2.9.1", "jshint": "^2.10.2",
"less": "2.7.1", "less": "2.7.1",
"lesshint": "^4.5.0", "lesshint": "^4.5.0",
"selenium-webdriver": "^3.6.0" "selenium-webdriver": "^3.6.0"

@ -0,0 +1,25 @@
/* jshint esversion: 6, node: true */
const Nacl = require('tweetnacl');
const keyPair = Nacl.box.keyPair();
console.log("You've just generated a new key pair for your support mailbox.");
console.log("The public key should first be added to your config.js file ('supportMailboxPublicKey'), then save and restart the server.");
console.log("Once restarted, administrators (specified with 'adminKeys' in config.js too) will be able to add the private key into their account. This can be done using the administration panel.");
console.log("You will have to send the private key to each administrator manually so that they can add it to their account.");
console.log();
console.log("WARNING: the public and private keys must come from the same key pair to have a working encrypted support mailbox.");
console.log();
console.log("NOTE: You can change the key pair at any time if you want to revoke access to the support mailbox. You just have to generate a new key pair using this file, and replace the value in config.js, and then send the new private key to the administrators of your choice.");
console.log();
console.log();
console.log("Your public key (add it to config.js):");
console.log(Nacl.util.encodeBase64(keyPair.publicKey));
console.log();
console.log();
console.log("Your private key (store it in a safe place and send it to your instance's admins):");
console.log(Nacl.util.encodeBase64(keyPair.secretKey));

@ -193,6 +193,7 @@ app.get('/api/config', function(req, res){
httpUnsafeOrigin: config.httpUnsafeOrigin, httpUnsafeOrigin: config.httpUnsafeOrigin,
adminEmail: config.adminEmail, adminEmail: config.adminEmail,
adminKeys: admins, adminKeys: admins,
supportMailbox: config.supportMailboxPublicKey
}, null, '\t'), }, null, '\t'),
'obj.httpSafeOrigin = ' + (function () { 'obj.httpSafeOrigin = ' + (function () {
if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; } if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; }

@ -1,5 +1,6 @@
@import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less'; @import (reference) '../../customize/src/less2/include/sidebar-layout.less';
@import (reference) '../../customize/src/less2/include/support.less';
&.cp-app-admin { &.cp-app-admin {
@ -9,6 +10,11 @@
@color: @colortheme_admin-color @color: @colortheme_admin-color
); );
.sidebar-layout_main(); .sidebar-layout_main();
.support_main();
.cp-hidden {
display: none !important;
}
display: flex; display: flex;
flex-flow: column; flex-flow: column;

@ -9,6 +9,8 @@ define([
'/customize/messages.js', '/customize/messages.js',
'/common/common-interface.js', '/common/common-interface.js',
'/common/common-util.js', '/common/common-util.js',
'/common/common-hash.js',
'/support/ui.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
@ -23,7 +25,9 @@ define([
h, h,
Messages, Messages,
UI, UI,
Util Util,
Hash,
Support
) )
{ {
var APP = {}; var APP = {};
@ -41,6 +45,10 @@ define([
'cp-admin-active-pads', 'cp-admin-active-pads',
'cp-admin-registered', 'cp-admin-registered',
'cp-admin-disk-usage', 'cp-admin-disk-usage',
],
'support': [
'cp-admin-support-list',
'cp-admin-support-init'
] ]
}; };
@ -94,7 +102,6 @@ define([
sFrameChan.query('Q_ADMIN_RPC', { sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ACTIVE_SESSIONS', cmd: 'ACTIVE_SESSIONS',
}, function (e, data) { }, function (e, data) {
console.log(e, data);
var total = data[0]; var total = data[0];
var ips = data[1]; var ips = data[1];
$div.append(h('pre', total + ' (' + ips + ')')); $div.append(h('pre', total + ' (' + ips + ')'));
@ -160,6 +167,108 @@ define([
return $div; return $div;
}; };
var supportKey = ApiConfig.supportMailbox;
create['support-list'] = function () {
if (!supportKey || !APP.privateKey) { return; }
var $div = makeBlock('support-list');
$div.addClass('cp-support-container');
var hashesById = {};
// Register to the "support" mailbox
common.mailbox.subscribe(['supportadmin'], {
onMessage: function (data) {
/*
Get ID of the ticket
If we already have a div for this ID
Push the message to the end of the ticket
If it's a new ticket ID
Make a new div for this ID
*/
var msg = data.content.msg;
var hash = data.content.hash;
var content = msg.content;
var id = content.id;
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]');
hashesById[id] = hashesById[id] || [];
if (hashesById[id].indexOf(hash) === -1) {
hashesById[id].push(data);
}
if (msg.type === 'CLOSE') {
// A ticket has been closed by the admins...
if (!$ticket.length) { return; }
$ticket.addClass('cp-support-list-closed');
$ticket.append(APP.support.makeCloseMessage(content, hash));
return;
}
if (msg.type !== 'TICKET') { return; }
if (!$ticket.length) {
$ticket = APP.support.makeTicket($div, content, function () {
var error = false;
hashesById[id].forEach(function (d) {
common.mailbox.dismiss(d, function (err) {
if (err) {
error = true;
console.error(err);
}
});
});
if (!error) { $ticket.remove(); }
});
}
$ticket.append(APP.support.makeMessage(content, hash));
}
});
return $div;
};
var checkAdminKey = function (priv) {
if (!supportKey) { return; }
return Hash.checkBoxKeyPair(priv, supportKey);
};
create['support-init'] = function () {
var $div = makeBlock('support-init');
if (!supportKey) {
$div.append(h('p', Messages.admin_supportInitHelp));
return $div;
}
if (!APP.privateKey || !checkAdminKey(APP.privateKey)) {
$div.append(h('p', Messages.admin_supportInitPrivate));
var error = h('div.cp-admin-support-error');
var input = h('input.cp-admin-add-private-key');
var button = h('button.btn.btn-primary', Messages.admin_supportAddKey);
if (APP.privateKey && !checkAdminKey(APP.privateKey)) {
$(error).text(Messages.admin_supportAddError);
}
$div.append(h('div', [
error,
input,
button
]));
$(button).click(function () {
var key = $(input).val();
if (!checkAdminKey(key)) {
$(input).val('');
return void $(error).text(Messages.admin_supportAddError);
}
sFrameChan.query("Q_ADMIN_MAILBOX", key, function () {
APP.privateKey = key;
$('.cp-admin-support-init').hide();
APP.$rightside.append(create['support-list']());
});
});
return $div;
}
return;
};
var hideCategories = function () { var hideCategories = function () {
APP.$rightside.find('> div').hide(); APP.$rightside.find('> div').hide();
}; };
@ -180,6 +289,7 @@ define([
var $category = $('<div>', {'class': 'cp-sidebarlayout-category'}).appendTo($categories); var $category = $('<div>', {'class': 'cp-sidebarlayout-category'}).appendTo($categories);
if (key === 'general') { $category.append($('<span>', {'class': 'fa fa-user-o'})); } if (key === 'general') { $category.append($('<span>', {'class': 'fa fa-user-o'})); }
if (key === 'stats') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); } if (key === 'stats') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); }
if (key === 'support') { $category.append($('<span>', {'class': 'fa fa-life-ring'})); }
if (key === active) { if (key === active) {
$category.addClass('cp-leftside-active'); $category.addClass('cp-leftside-active');
@ -236,8 +346,10 @@ define([
return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden'); return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden');
} }
APP.privateKey = privateData.supportPrivateKey;
APP.origin = privateData.origin; APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly; APP.readOnly = privateData.readOnly;
APP.support = Support.create(common, true);
// Content // Content
var $rightside = APP.$rightside; var $rightside = APP.$rightside;

@ -38,6 +38,9 @@ define([
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {
var addRpc = function (sframeChan, Cryptpad/*, Utils*/) { var addRpc = function (sframeChan, Cryptpad/*, Utils*/) {
// Adding a new avatar from the profile: pin it and store it in the object // Adding a new avatar from the profile: pin it and store it in the object
sframeChan.on('Q_ADMIN_MAILBOX', function (data, cb) {
Cryptpad.addAdminMailbox(data, cb);
});
sframeChan.on('Q_ADMIN_RPC', function (data, cb) { sframeChan.on('Q_ADMIN_RPC', function (data, cb) {
Cryptpad.adminRpc(data, cb); Cryptpad.adminRpc(data, cb);
}); });

@ -371,6 +371,20 @@ define([
return cb(true); return cb(true);
}, "version 2 hash failed to parse correctly"); }, "version 2 hash failed to parse correctly");
assert(function (cb) {
var x;
var set_x = function (v) {
x = v;
};
Util.mkAsync(set_x)(7);
set_x(5);
Util.mkAsync(function (expected) {
cb(x === expected);
})(7);
}, "test mkAsync");
assert(function (cb) { assert(function (cb) {
Wire.create({ Wire.create({
constructor: function (cb) { constructor: function (cb) {

@ -8,7 +8,7 @@ define([
module.main = function (userDoc, cb) { module.main = function (userDoc, cb) {
var mode = userDoc.highlightMode || 'gfm'; var mode = userDoc.highlightMode || 'gfm';
var content = userDoc.content; var content = userDoc.content;
module.type = SFCodeMirror.getContentExtension(mode); module.ext = SFCodeMirror.getContentExtension(mode);
cb(SFCodeMirror.fileExporter(content)); cb(SFCodeMirror.fileExporter(content));
}; };

@ -363,7 +363,15 @@ define([
}); });
framework.setFileExporter(CodeMirror.getContentExtension, CodeMirror.fileExporter); framework.setFileExporter(CodeMirror.getContentExtension, CodeMirror.fileExporter);
framework.setFileImporter({}, CodeMirror.fileImporter); framework.setFileImporter({}, function () {
/* setFileImporter currently takes a function with the following signature:
(content, file) => {}
I used 'apply' with 'arguments' to avoid breaking things if this API ever changes.
*/
var ret = CodeMirror.fileImporter.apply(null, Array.prototype.slice.call(arguments));
previewPane.modeChange(ret.mode);
return ret;
});
framework.setNormalizer(function (c) { framework.setNormalizer(function (c) {
return { return {

@ -20,7 +20,7 @@ define(function() {
* users and these users will be redirected to the login page if they still try to access * users and these users will be redirected to the login page if they still try to access
* the app * the app
*/ */
config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'sheet']; config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'sheet', 'notifications'];
/* CryptPad is available is multiple languages, but only English and French are maintained /* CryptPad is available is multiple languages, but only English and French are maintained
* by the developers. The other languages may be outdated, and any missing string for a langauge * by the developers. The other languages may be outdated, and any missing string for a langauge

@ -17,6 +17,6 @@ define(function () {
// Sub // Sub
plan: 'CryptPad_plan', plan: 'CryptPad_plan',
// Apps // Apps
criticalApps: ['profile', 'settings', 'debug', 'admin'] criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications']
}; };
}); });

@ -85,6 +85,34 @@ define([
return id; return id;
}; };
/* Given a base64-encoded public key, deterministically derive a channel id
Used for support mailboxes
*/
Hash.getChannelIdFromKey = function (publicKey) {
if (!publicKey) { return; }
return uint8ArrayToHex(Hash.decodeBase64(publicKey).subarray(0,16));
};
/* Given a base64-encoded asymmetric private key
derive the corresponding public key
*/
Hash.getBoxPublicFromSecret = function (priv) {
if (!priv) { return; }
var u8_priv = Hash.decodeBase64(priv);
var pair = Nacl.box.keyPair.fromSecretKey(u8_priv);
return Hash.encodeBase64(pair.publicKey);
};
/* Given a base64-encoded private key and public key
check that the keys are part of a valid keypair
*/
Hash.checkBoxKeyPair = function (priv, pub) {
if (!pub || !priv) { return false; }
var u8_priv = Hash.decodeBase64(priv);
var pair = Nacl.box.keyPair.fromSecretKey(u8_priv);
return pub === Hash.encodeBase64(pair.publicKey);
};
Hash.createRandomHash = function (type, password) { Hash.createRandomHash = function (type, password) {
var cryptor; var cryptor;
if (type === 'file') { if (type === 'file') {

@ -799,6 +799,11 @@ define([
// forever, this is a solution which just searches for tooltips which have no corrisponding element and removes // forever, this is a solution which just searches for tooltips which have no corrisponding element and removes
// them. // them.
$('.tippy-popper').each(function (i, el) { $('.tippy-popper').each(function (i, el) {
if (el._tippy && el._tippy.reference && document.body.contains(el._tippy.reference)) {
el._tippy.destroy();
el.remove();
return;
}
if ($('[aria-describedby=' + el.getAttribute('id') + ']').length === 0) { if ($('[aria-describedby=' + el.getAttribute('id') + ']').length === 0) {
el.remove(); el.remove();
} }
@ -849,6 +854,9 @@ define([
mutations.forEach(function(mutation) { mutations.forEach(function(mutation) {
if (mutation.type === "childList") { if (mutation.type === "childList") {
for (var i = 0; i < mutation.addedNodes.length; i++) { 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); $(mutation.addedNodes[i]).find('[title]').each(addTippy);
} }

@ -451,9 +451,7 @@ define([
var txid = parsed[1]; var txid = parsed[1];
var req = getRangeRequest(txid); var req = getRangeRequest(txid);
var type = parsed[0]; var type = parsed[0];
if (!req) { if (!req) { return; }
return void console.error("received response to unknown request");
}
if (!req.cb) { if (!req.cb) {
// This is the initial history for a pad chat // This is the initial history for a pad chat

@ -196,8 +196,8 @@ define([
}; };
Thumb.initPadThumbnails = function (common, opts) { Thumb.initPadThumbnails = function (common, opts) {
if (!opts.href || !opts.getContent) { if (!opts.type || !opts.getContent) {
throw new Error("href and getContent are needed for thumbnails"); throw new Error("type and getContent are needed for thumbnails");
} }
var oldThumbnailState; var oldThumbnailState;
var mkThumbnail = function () { var mkThumbnail = function () {
@ -230,9 +230,15 @@ define([
if (!Visible.currently()) { to = window.setTimeout(interval, Thumb.UPDATE_FIRST); } if (!Visible.currently()) { to = window.setTimeout(interval, Thumb.UPDATE_FIRST); }
}; };
var addThumbnail = function (err, thumb, $span, cb) { var addThumbnail = function (err, thumb, $span, cb) {
var u8 = Nacl.util.decodeBase64(thumb.split(',')[1]);
var blob = new Blob([u8], {
type: 'image/png'
});
var url = URL.createObjectURL(blob);
var img = new Image(); var img = new Image();
img.src = thumb.slice(0,5) === 'data:' ? thumb : 'data:image/png;base64,'+thumb; img.src = url;
$span.find('.cp-icon').hide(); $span.find('.cp-icon').hide();
$span.prepend(img); $span.prepend(img);
cb($(img)); cb($(img));

@ -119,15 +119,32 @@ define([
$('<label>', {'for': 'cp-app-prop-owners'}).text(Messages.creation_owners) $('<label>', {'for': 'cp-app-prop-owners'}).text(Messages.creation_owners)
.appendTo($d); .appendTo($d);
var owners = Messages.creation_noOwner; var owners = Messages.creation_noOwner;
var edPublic = common.getMetadataMgr().getPrivateData().edPublic; var priv = common.getMetadataMgr().getPrivateData();
var edPublic = priv.edPublic;
var owned = false; var owned = false;
if (data.owners && data.owners.length) { if (data.owners && data.owners.length) {
if (data.owners.indexOf(edPublic) !== -1) { if (data.owners.indexOf(edPublic) !== -1) {
owners = Messages.yourself;
owned = true; owned = true;
} else {
owners = Messages.creation_ownedByOther;
} }
var names = [];
var strangers = 0;
data.owners.forEach(function (ed) {
// If a friend is an owner, add their name to the list
// otherwise, increment the list of strangers
if (!Object.keys(priv.friends || {}).some(function (c) {
var friend = priv.friends[c] || {};
if (friend.edPublic !== ed) { return; }
var name = c === 'me' ? Messages.yourself : friend.displayName;
names.push(name);
return true;
})) {
strangers++;
}
});
if (strangers) {
names.push(Messages._getKey('properties_unknownUser', [strangers]));
}
owners = names.join(', ');
} }
$d.append(UI.dialog.selectable(owners, { $d.append(UI.dialog.selectable(owners, {
id: 'cp-app-prop-owners', id: 'cp-app-prop-owners',
@ -162,7 +179,7 @@ define([
} }
var parsed = Hash.parsePadUrl(data.href || data.roHref); var parsed = Hash.parsePadUrl(data.href || data.roHref);
if (!data.noEditPassword && owned && parsed.hashData.type === 'pad') { if (!data.noEditPassword && owned && parsed.hashData.type === 'pad' && parsed.type !== "sheet") { // FIXME SHEET fix password change for sheets
var sframeChan = common.getSframeChannel(); var sframeChan = common.getSframeChannel();
var changePwTitle = Messages.properties_changePassword; var changePwTitle = Messages.properties_changePassword;
var changePwConfirm = Messages.properties_confirmChange; var changePwConfirm = Messages.properties_confirmChange;
@ -289,7 +306,7 @@ define([
id: 'cp-app-prop-size', id: 'cp-app-prop-size',
})); }));
if (data.sharedFolder) { // XXX debug if (data.sharedFolder) {
$('<label>', {'for': 'cp-app-prop-channel'}).text('Channel ID').appendTo($d); $('<label>', {'for': 'cp-app-prop-channel'}).text('Channel ID').appendTo($d);
if (AppConfig.pinBugRecovery) { $d.append(h('p', AppConfig.pinBugRecovery)); } if (AppConfig.pinBugRecovery) { $d.append(h('p', AppConfig.pinBugRecovery)); }
$d.append(UI.dialog.selectable(data.channel, { $d.append(UI.dialog.selectable(data.channel, {
@ -325,7 +342,7 @@ define([
}); });
}; };
var getFriendsList = function (config) { var getFriendsList = function (config, onShare) {
var common = config.common; var common = config.common;
var title = config.title; var title = config.title;
var friends = config.friends; var friends = config.friends;
@ -412,6 +429,8 @@ define([
if (!friend.notifications || !friend.curvePublic) { return; } if (!friend.notifications || !friend.curvePublic) { return; }
common.mailbox.sendTo("SHARE_PAD", { common.mailbox.sendTo("SHARE_PAD", {
href: href, href: href,
password: config.password,
isTemplate: config.isTemplate,
name: myName, name: myName,
title: title title: title
}, { }, {
@ -435,6 +454,9 @@ define([
return smallCurves.indexOf(curve) !== -1; return smallCurves.indexOf(curve) !== -1;
}); });
common.setAttribute(['general', 'share-friends'], order); common.setAttribute(['general', 'share-friends'], order);
if (onShare) {
onShare.fire();
}
}); });
$nav.append(button); $nav.append(button);
} }
@ -511,8 +533,10 @@ define([
// Share link tab // Share link tab
var hasFriends = Object.keys(config.friends || {}).length !== 0; var hasFriends = Object.keys(config.friends || {}).length !== 0;
var friendsList = hasFriends ? getFriendsList(config) : undefined; var onFriendShare = Util.mkEvent();
var friendsList = hasFriends ? getFriendsList(config, onFriendShare) : undefined;
var friendsUIClass = hasFriends ? '.cp-share-columns' : ''; var friendsUIClass = hasFriends ? '.cp-share-columns' : '';
var link = h('div.cp-share-modal' + friendsUIClass, [ var link = h('div.cp-share-modal' + friendsUIClass, [
h('div.cp-share-column', [ h('div.cp-share-column', [
hasFriends ? h('p', Messages.share_description) : undefined, hasFriends ? h('p', Messages.share_description) : undefined,
@ -546,11 +570,12 @@ define([
present: present present: present
}); });
}; };
onFriendShare.reg(saveValue);
var getLinkValue = function (initValue) { var getLinkValue = function (initValue) {
var val = initValue || {}; var val = initValue || {};
var edit = initValue ? val.edit : Util.isChecked($(link).find('#cp-share-editable-true')); var edit = val.edit !== undefined ? val.edit : Util.isChecked($(link).find('#cp-share-editable-true'));
var embed = initValue ? val.embed : Util.isChecked($(link).find('#cp-share-embed')); var embed = val.embed !== undefined ? val.embed : Util.isChecked($(link).find('#cp-share-embed'));
var present = initValue ? val.present : Util.isChecked($(link).find('#cp-share-present')); var present = val.present !== undefined ? val.present : Util.isChecked($(link).find('#cp-share-present'));
var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash; var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash;
var href = origin + pathname + '#' + hash; var href = origin + pathname + '#' + hash;
@ -700,7 +725,10 @@ define([
}, },
keys: [13] keys: [13]
}]; }];
var frameLink = UI.dialog.customModal(link, {buttons: linkButtons}); var frameLink = UI.dialog.customModal(link, {
buttons: linkButtons,
onClose: config.onClose,
});
// Embed tab // Embed tab
var embed = h('div.cp-share-modal', [ var embed = h('div.cp-share-modal', [
@ -727,7 +755,10 @@ define([
}, },
keys: [13] keys: [13]
}]; }];
var frameEmbed = UI.dialog.customModal(embed, { buttons: embedButtons}); var frameEmbed = UI.dialog.customModal(embed, {
buttons: embedButtons,
onClose: config.onClose,
});
// Create modal // Create modal
var tabs = [{ var tabs = [{
@ -1737,16 +1768,20 @@ define([
var pressed = ''; var pressed = '';
var to; var to;
$container.keydown(function (e) { $container.keydown(function (e) {
var $value = $innerblock.find('[data-value].cp-dropdown-element-active'); var $value = $innerblock.find('[data-value].cp-dropdown-element-active:visible');
if (e.which === 38) { // Up if (e.which === 38) { // Up
if ($value.length) { if ($value.length) {
$value.mouseleave();
var $prev = $value.prev(); var $prev = $value.prev();
$prev.mouseenter();
setActive($prev); setActive($prev);
} }
} }
if (e.which === 40) { // Down if (e.which === 40) { // Down
if ($value.length) { if ($value.length) {
$value.mouseleave();
var $next = $value.next(); var $next = $value.next();
$next.mouseenter();
setActive($next); setActive($next);
} }
} }
@ -1757,6 +1792,7 @@ define([
} }
} }
if (e.which === 27) { // Esc if (e.which === 27) { // Esc
$value.mouseleave();
hide(); hide();
} }
}); });
@ -1861,6 +1897,13 @@ define([
content: h('span', Messages.adminPage || 'Admin') content: h('span', Messages.adminPage || 'Admin')
}); });
} }
if (padType !== 'support' && accountName && Config.supportMailbox) {
options.push({
tag: 'a',
attributes: {'class': 'cp-toolbar-menu-support fa fa-life-ring'},
content: h('span', Messages.supportPage || 'Support')
});
}
// Add login or logout button depending on the current status // Add login or logout button depending on the current status
if (accountName) { if (accountName) {
options.push({ options.push({
@ -1956,6 +1999,13 @@ define([
window.parent.location = origin+'/settings/'; window.parent.location = origin+'/settings/';
} }
}); });
$userAdmin.find('a.cp-toolbar-menu-support').click(function () {
if (padType) {
window.open(origin+'/support/');
} else {
window.parent.location = origin+'/support/';
}
});
$userAdmin.find('a.cp-toolbar-menu-admin').click(function () { $userAdmin.find('a.cp-toolbar-menu-admin').click(function () {
if (padType) { if (padType) {
window.open(origin+'/admin/'); window.open(origin+'/admin/');
@ -2054,6 +2104,9 @@ define([
}; };
UIElements.createNewPadModal = function (common) { UIElements.createNewPadModal = function (common) {
// if in drive, show new pad modal instead
if ($("body.cp-app-drive").length !== 0) { return void $(".cp-app-drive-element-row.cp-app-drive-new-ghost").click(); }
var $modal = UIElements.createModal({ var $modal = UIElements.createModal({
id: 'cp-app-toolbar-creation-dialog', id: 'cp-app-toolbar-creation-dialog',
$body: $('body') $body: $('body')
@ -2716,8 +2769,12 @@ define([
UIElements.displayCrowdfunding(common); UIElements.displayCrowdfunding(common);
modal.delete(); modal.delete();
}); });
var waitingForStoringCb = false;
$(store).click(function () { $(store).click(function () {
if (waitingForStoringCb) { return; }
waitingForStoringCb = true;
common.getSframeChannel().query("Q_AUTOSTORE_STORE", null, function (err, obj) { common.getSframeChannel().query("Q_AUTOSTORE_STORE", null, function (err, obj) {
waitingForStoringCb = false;
var error = err || (obj && obj.error); var error = err || (obj && obj.error);
if (error) { if (error) {
if (error === 'E_OVER_LIMIT') { if (error === 'E_OVER_LIMIT') {
@ -2804,11 +2861,27 @@ define([
'aria-labelledBy': 'dropdownMenu', 'aria-labelledBy': 'dropdownMenu',
'style': 'display:block;position:static;margin-bottom:5px;' 'style': 'display:block;position:static;margin-bottom:5px;'
}, [ }, [
h('li', h('a.dropdown-item', { h('li', h('a.cp-app-code-context-saveindrive.dropdown-item', {
'tabindex': '-1',
'data-icon': "fa-cloud-upload",
}, Messages.pad_mediatagImport)),
h('li', h('a.cp-app-code-context-download.dropdown-item', {
'tabindex': '-1', 'tabindex': '-1',
}, Messages.pad_mediatagImport)) 'data-icon': "fa-download",
}, Messages.download_mt_button)),
]) ])
]); ]);
// create the icon for each contextmenu option
$(menu).find("li a.dropdown-item").each(function (i, el) {
var $icon = $("<span>");
if ($(el).attr('data-icon')) {
var font = $(el).attr('data-icon').indexOf('cptools') === 0 ? 'cptools' : 'fa';
$icon.addClass(font).addClass($(el).attr('data-icon'));
} else {
$icon.text($(el).text());
}
$(el).prepend($icon);
});
var m = createContextMenu(menu); var m = createContextMenu(menu);
mediatagContextMenu = m; mediatagContextMenu = m;
@ -2818,7 +2891,13 @@ define([
e.stopPropagation(); e.stopPropagation();
m.hide(); m.hide();
var $mt = $menu.data('mediatag'); var $mt = $menu.data('mediatag');
common.importMediaTag($mt); if ($(this).hasClass("cp-app-code-context-saveindrive")) {
common.importMediaTag($mt);
}
else if ($(this).hasClass("cp-app-code-context-download")) {
var media = $mt[0]._mediaObject;
window.saveAs(media._blob.content, media.name);
}
}); });
return m; return m;

@ -2,6 +2,15 @@
define([], function () { define([], function () {
var Util = window.CryptPad_Util = {}; var Util = window.CryptPad_Util = {};
Util.mkAsync = function (f) {
return function () {
var args = Array.prototype.slice.call(arguments);
setTimeout(function () {
f.apply(null, args);
});
};
};
// If once is true, after the event has been fired, any further handlers which are // If once is true, after the event has been fired, any further handlers which are
// registered will fire immediately, and this type of event cannot be fired twice. // registered will fire immediately, and this type of event cannot be fired twice.
Util.mkEvent = function (once) { Util.mkEvent = function (once) {
@ -310,6 +319,12 @@ define([], function () {
return window.innerHeight < 800 || window.innerWidth < 800; return window.innerHeight < 800 || window.innerWidth < 800;
}; };
Util.stripTags = function (text) {
var div = document.createElement("div");
div.innerHTML = text;
return div.innerText;
};
return Util; return Util;
}); });
}(self)); }(self));

@ -620,6 +620,9 @@ define([
common.adminRpc = function (data, cb) { common.adminRpc = function (data, cb) {
postMessage("ADMIN_RPC", data, cb); postMessage("ADMIN_RPC", data, cb);
}; };
common.addAdminMailbox = function (data, cb) {
postMessage("ADMIN_ADD_MAILBOX", data, cb);
};
// Network // Network
common.onNetworkDisconnect = Util.mkEvent(); common.onNetworkDisconnect = Util.mkEvent();
@ -690,6 +693,13 @@ define([
pad.onConnectEvent = Util.mkEvent(); pad.onConnectEvent = Util.mkEvent();
pad.onErrorEvent = Util.mkEvent(); pad.onErrorEvent = Util.mkEvent();
pad.requestAccess = function (data, cb) {
postMessage("REQUEST_PAD_ACCESS", data, cb);
};
pad.giveAccess = function (data, cb) {
postMessage("GIVE_PAD_ACCESS", data, cb);
};
common.changePadPassword = function (Crypt, href, newPassword, edPublic, cb) { common.changePadPassword = function (Crypt, href, newPassword, edPublic, cb) {
if (!href) { return void cb({ error: 'EINVAL_HREF' }); } if (!href) { return void cb({ error: 'EINVAL_HREF' }); }
var parsed = Hash.parsePadUrl(href); var parsed = Hash.parsePadUrl(href);

@ -1,51 +0,0 @@
define([
'/common/curve.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
], function (Curve, Listmap) {
var Edit = {};
Edit.create = function (config, cb) { //network, channel, theirs, mine, cb) {
var network = config.network;
var channel = config.channel;
var keys = config.keys;
try {
var encryptor = Curve.createEncryptor(keys);
var lm = Listmap.create({
network: network,
data: {},
channel: channel,
readOnly: false,
validateKey: keys.validateKey || undefined,
crypto: encryptor,
userName: 'lol',
logLevel: 1,
});
var done = function () {
// TODO make this abort and disconnect the session after the
// user has finished making changes to the object, and they
// have propagated.
};
lm.proxy
.on('create', function () {
console.log('created');
})
.on('ready', function () {
console.log('ready');
cb(lm, done);
})
.on('disconnect', function () {
console.log('disconnected');
})
.on('change', [], function (o, n, p) {
console.log(o, n, p);
});
} catch (e) {
console.error(e);
}
};
return Edit;
});

@ -15,6 +15,7 @@ define([
var DiffDOM = window.diffDOM; var DiffDOM = window.diffDOM;
var renderer = new Marked.Renderer(); var renderer = new Marked.Renderer();
var restrictedRenderer = new Marked.Renderer();
var Mermaid = { var Mermaid = {
init: function () {} init: function () {}
@ -61,13 +62,18 @@ define([
return h('div.cp-md-toc', content).outerHTML; return h('div.cp-md-toc', content).outerHTML;
}; };
DiffMd.render = function (md, sanitize) { DiffMd.render = function (md, sanitize, restrictedMd) {
Marked.setOptions({
renderer: restrictedMd ? restrictedRenderer : renderer,
});
var r = Marked(md, { var r = Marked(md, {
sanitize: sanitize sanitize: sanitize
}); });
// Add Table of Content // Add Table of Content
r = r.replace(/<div class="cp-md-toc"><\/div>/g, getTOC()); if (!restrictedMd) {
r = r.replace(/<div class="cp-md-toc"><\/div>/g, getTOC());
}
toc = []; toc = [];
return r; return r;
@ -83,6 +89,7 @@ define([
return defaultCode.apply(renderer, arguments); return defaultCode.apply(renderer, arguments);
} }
}; };
restrictedRenderer.code = renderer.code;
renderer.heading = function (text, level) { renderer.heading = function (text, level) {
var i = 0; var i = 0;
@ -99,10 +106,13 @@ define([
toc.push({ toc.push({
level: level, level: level,
id: id, id: id,
title: text title: Util.stripTags(text)
}); });
return "<h" + level + " id=\"" + id + "\"><a href=\"#" + id + "\" class=\"anchor\"></a>" + text + "</h" + level + ">"; return "<h" + level + " id=\"" + id + "\"><a href=\"#" + id + "\" class=\"anchor\"></a>" + text + "</h" + level + ">";
}; };
restrictedRenderer.heading = function (text) {
return text;
};
// Tasks list // Tasks list
var checkedTaskItemPtn = /^\s*(<p>)?\[[xX]\](<\/p>)?\s*/; var checkedTaskItemPtn = /^\s*(<p>)?\[[xX]\](<\/p>)?\s*/;
@ -122,16 +132,23 @@ define([
} }
if (!isCheckedTaskItem && !isUncheckedTaskItem && hasBogusInput) { if (!isCheckedTaskItem && !isUncheckedTaskItem && hasBogusInput) {
if (/checked/.test(text)) { if (/checked/.test(text)) {
text = text.replace(bogusCheckPtn, text = text.replace(bogusCheckPtn,
'<i class="fa fa-check-square" aria-hidden="true"></i>') + '\n'; '<i class="fa fa-check-square" aria-hidden="true"></i>') + '\n';
} else if (/disabled/.test(text)) { } else if (/disabled/.test(text)) {
text = text.replace(bogusCheckPtn, text = text.replace(bogusCheckPtn,
'<i class="fa fa-square-o" aria-hidden="true"></i>') + '\n'; '<i class="fa fa-square-o" aria-hidden="true"></i>') + '\n';
} }
} }
var cls = (isCheckedTaskItem || isUncheckedTaskItem || hasBogusInput) ? ' class="todo-list-item"' : ''; var cls = (isCheckedTaskItem || isUncheckedTaskItem || hasBogusInput) ? ' class="todo-list-item"' : '';
return '<li'+ cls + '>' + text + '</li>\n'; return '<li'+ cls + '>' + text + '</li>\n';
}; };
restrictedRenderer.listitem = function (text) {
if (bogusCheckPtn.test(text)) {
text = text.replace(bogusCheckPtn, '');
}
return '<li>' + text + '</li>\n';
};
renderer.image = function (href, title, text) { renderer.image = function (href, title, text) {
if (href.slice(0,6) === '/file/') { if (href.slice(0,6) === '/file/') {
// DEPRECATED // DEPRECATED
@ -156,12 +173,19 @@ define([
out += this.options.xhtml ? '/>' : '>'; out += this.options.xhtml ? '/>' : '>';
return out; return out;
}; };
restrictedRenderer.image = renderer.image;
var renderParagraph = function (p) {
return /<media\-tag[\s\S]*>/i.test(p)? p + '\n': '<p>' + p + '</p>\n';
};
renderer.paragraph = function (p) { renderer.paragraph = function (p) {
if (p === '[TOC]') { if (p === '[TOC]') {
return '<p><div class="cp-md-toc"></div></p>'; return '<p><div class="cp-md-toc"></div></p>';
} }
return /<media\-tag[\s\S]*>/i.test(p)? p + '\n': '<p>' + p + '</p>\n'; return renderParagraph(p);
};
restrictedRenderer.paragraph = function (p) {
return renderParagraph(p);
}; };
var MutationObserver = window.MutationObserver; var MutationObserver = window.MutationObserver;

@ -30,9 +30,22 @@
}; };
var isplainTextFile = function (metadata) {
// does its type begins with "text/"
if (metadata.type.indexOf("text/") === 0) { return true; }
// no type and no file extension -> let's guess it's plain text
var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(metadata.name) || [];
if (!metadata.type && !parsedName[2]) { return true; }
// other exceptions
if (metadata.type === 'application/x-javascript') { return true; }
if (metadata.type === 'application/xml') { return true; }
return false;
};
// Default config, can be overriden per media-tag call // Default config, can be overriden per media-tag call
var config = { var config = {
allowed: [ allowed: [
'text/plain',
'image/png', 'image/png',
'image/jpeg', 'image/jpeg',
'image/jpg', 'image/jpg',
@ -53,6 +66,23 @@
text: "Download" text: "Download"
}, },
Plugins: { Plugins: {
/**
* @param {object} metadataObject {name, metadatatype, owners} containing metadata of the file
* @param {strint} url Url of the blob object
* @param {Blob} content Blob object containing the data of the file
* @param {object} cfg Object {Plugins, allowed, download, pdf} containing infos about plugins
* @param {function} cb Callback function: (err, pluginElement) => {}
*/
text: function (metadata, url, content, cfg, cb) {
var plainText = document.createElement('div');
plainText.className = "plain-text-reader";
var reader = new FileReader();
reader.addEventListener('loadend', function (e) {
plainText.innerText = e.srcElement.result;
cb(void 0, plainText);
});
reader.readAsText(content);
},
image: function (metadata, url, content, cfg, cb) { image: function (metadata, url, content, cfg, cb) {
var img = document.createElement('img'); var img = document.createElement('img');
img.setAttribute('src', url); img.setAttribute('src', url);
@ -271,6 +301,9 @@
var blob = decrypted.content; var blob = decrypted.content;
var mediaType = getType(mediaObject, metadata, cfg); var mediaType = getType(mediaObject, metadata, cfg);
if (isplainTextFile(metadata)) {
mediaType = "text";
}
if (mediaType === 'application') { if (mediaType === 'application') {
mediaType = mediaObject.extension; mediaType = mediaObject.extension;

@ -49,7 +49,7 @@ define([
// We want to merge an edit pad: check if we have the same channel // We want to merge an edit pad: check if we have the same channel
// but read-only and upgrade it in that case // but read-only and upgrade it in that case
datas.forEach(function (pad) { datas.forEach(function (pad) {
if (!pad.href) { data.href = pad.href; } if (pad.data && !pad.data.href) { pad.data.href = data.href; }
}); });
return; return;
} }

@ -151,7 +151,7 @@ define([
}); });
try { try {
var $d = $(d); var $d = $(d);
DiffMd.apply(DiffMd.render(md || '', true), $d, common); DiffMd.apply(DiffMd.render(md || '', true, true), $d, common);
$d.addClass("cp-app-contacts-content"); $d.addClass("cp-app-contacts-content");
// override link clicking, because we're in an iframe // override link clicking, because we're in an iframe
@ -197,7 +197,7 @@ define([
var getChat = function (id) { var getChat = function (id) {
return $messages.find(dataQuery(id)); return $messages.find(dataQuery(id));
}; };
var scrollChatToBottom = function () { var scrollChatToBottom = function () {
var $messagebox = $('.cp-app-contacts-messages'); var $messagebox = $('.cp-app-contacts-messages');
$messagebox.scrollTop($messagebox[0].scrollHeight); $messagebox.scrollTop($messagebox[0].scrollHeight);

@ -191,9 +191,6 @@ define([
userObject.version = version = 8; userObject.version = version = 8;
} }
}).nThen(function () { }).nThen(function () {
if (!AppConfig.migrateFriends) { return; } // XXX
// Migration 9: send our mailbox channel to existing friends // Migration 9: send our mailbox channel to existing friends
var migrateFriends = function () { var migrateFriends = function () {
var network = store.network; var network = store.network;

@ -6,141 +6,141 @@ define([
// mode language (extension) // mode language (extension)
var list = Modes.list = [ var list = Modes.list = [
"APL apl .apl", "APL apl .apl",
"ASCII-Armor asciiarmor", "ASCII-Armor asciiarmor .asc",
"ASN.1 asn.1", "ASN.1 asn.1 .asn1",
"Asterisk asterisk", "Asterisk asterisk",
"Brainfuck brainfuck .b", "Brainfuck brainfuck .b",
"C text/x-csrc .c", "C text/x-csrc .c",
"C text/x-c++src .cpp", "C text/x-c++src .cpp",
"C-like clike", "C-like clike .c",
"Clojure clojure", "Clojure clojure .clj",
"CMake cmake", "CMake cmake _", /* no extension */
"COBOL cobol", "COBOL cobol .cbl",
"CoffeeScript coffeescript", "CoffeeScript coffeescript .coffee",
"Common_Lisp commonlisp", "Common_Lisp commonlisp .lisp",
"Crystal crystal", "Crystal crystal .cr",
"CSS css .css", "CSS css .css",
"Cypher cypher", "Cypher cypher .cypher",
"D d", "D d .d",
"Dart dart", "Dart dart .dart",
"Diff diff", "Diff diff .diff",
"Django django", "Django django .py",
"Dockerfile dockerfile", "Dockerfile dockerfile _", /* no extension */
"DTD dtd", "DTD dtd .dtd",
"Dylan dylan", "Dylan dylan .dylan",
"EBNF ebnf", "EBNF ebnf .ebnf",
"ECL ecl", "ECL ecl .ecl",
"Eiffel eiffel", "Eiffel eiffel .e",
"Elm elm .elm", "Elm elm .elm",
"Erlang erlang", "Erlang erlang .erl",
"Factor factor", "Factor factor .factor",
"FCL fcl", "FCL fcl .fcl",
"Forth forth", "Forth forth .fs",
"Fortran fortran", "Fortran fortran .f90",
"GAS gas", "GAS gas .gas",
"Gherkin gherkin", "Gherkin gherkin .feature",
"Go go", "Go go .go",
"Groovy groovy", "Groovy groovy .groovy",
"Haml haml", "Haml haml .haml",
"Handlebars handlebars", "Handlebars handlebars .hbs",
"Haskell haskell .hs", "Haskell haskell .hs",
"Haskell-Literate haskell-literate", "Haskell-Literate haskell-literate .lhs",
"Haxe haxe", "Haxe haxe .hx",
"HTML htmlmixed .html", "HTML htmlmixed .html",
"HTTP http", "HTTP http _", /* no extension */
"IDL idl", "IDL idl .idl",
"JADE jade", "JADE jade .jade",
"Java text/x-java .java", "Java text/x-java .java",
"JavaScript javascript .js", "JavaScript javascript .js",
"Jinja2 jinja2", "Jinja2 jinja2 .j2",
"JSX jsx .jsx", "JSX jsx .jsx",
"Julia julia", "Julia julia .jl",
"LiveScript livescript", "LiveScript livescript .ls",
"Lua lua", "Lua lua .lua",
"Markdown gfm .md", "Markdown gfm .md",
//"markdown markdown .md", //"markdown markdown .md",
"Mathematica mathematica", "Mathematica mathematica .nb",
"mIRC mirc", "mIRC mirc .irc",
"ML mllike", "ML mllike _", /* no extension */
"Modelica modelica", "Modelica modelica .mo",
"MscGen mscgen", "MscGen mscgen .mscgen",
"MUMPS mumps", "MUMPS mumps .m",
"Nginx nginx", "Nginx nginx .conf",
"NSIS nsis", "NSIS nsis .nsi",
"N-Triples ntriples", "N-Triples ntriples .nq",
"Objective-C text/x-objectivec .m", "Objective-C text/x-objectivec .m",
"Octave octave", "Octave octave .m",
"Org-mode orgmode .org", "Org-mode orgmode .org",
"Oz oz", "Oz oz .oz",
"Pascal pascal", "Pascal pascal .pas",
"PEG.js pegjs", "PEG.js pegjs .pegjs",
"Perl perl", "Perl perl .pl",
"PHP php", "PHP php .php",
"Pig pig", "Pig pig .pig",
"PowerShell powershell", "PowerShell powershell .ps1",
"Properties properties", "Properties properties .properties",
"Protocol_Buffers protobuf", "Protocol_Buffers protobuf .proto",
"Puppet puppet", "Puppet puppet .pp",
"Python python .py", "Python python .py",
"Q q", "Q q .q",
"R r", "R r .r",
"RPM rpm", "RPM rpm .rpm",
"RST rst", "RST rst .rst",
"Ruby ruby", "Ruby ruby .rb",
"Rust rust", "Rust rust .rs",
"Sass sass", "Sass sass .sass",
"Scheme scheme .scm", "Scheme scheme .scm",
"Shell shell .sh", "Shell shell .sh",
"Sieve sieve", "Sieve sieve .sieve",
"Slim slim", "Slim slim .slim",
"Smalltalk smalltalk", "Smalltalk smalltalk _", /* no extension */
"Smarty smarty", "Smarty smarty _", /* no extension */
"Solr solr", "Solr solr _", /* no extension */
"Soy soy", "Soy soy .soy",
"SPARQL sparql", "SPARQL sparql .rq",
"Spreadsheet spreadsheet", "Spreadsheet spreadsheet .xls",
"SQL sql", "SQL sql .sql",
"sTeX stex", "sTeX stex .stex",
"Stylus stylus", "Stylus stylus .styl",
"Swift swift", "Swift swift .swift",
"Tcl tcl", "Tcl tcl .tcl",
"Text text .txt", "Text text .txt",
"Textile textile", "Textile textile .textile",
"TiddlyWiki tiddlywiki", "TiddlyWiki tiddlywiki .tw",
"Tiki tiki", "Tiki tiki _", /* no extension */
"TOML toml", "TOML toml .toml",
"Tornado tornado", "Tornado tornado .tornado",
"troff troff", "troff troff .troff",
"TTCN ttcn", "TTCN ttcn",
"TTCN-cfg ttcn-cfg", "TTCN-cfg ttcn-cfg",
"Turtle turtle", "Turtle turtle .ttl",
"Twig twig", "Twig twig .twig",
"Visual_Basic vb", "Visual_Basic vb .vb",
"VBScript vbscript", "VBScript vbscript .vbs",
"Velocity velocity", "Velocity velocity .vm",
"Verilog verilog", "Verilog verilog .v",
"VHDL vhdl", "VHDL vhdl .vhdl",
"Vue vue", "Vue vue .vue",
"XML xml", "XML xml .xml",
//"xwiki xwiki21", //"xwiki xwiki21",
"XQuery xquery", "XQuery xquery .xquery",
"YAML yaml .yaml", "YAML yaml .yaml",
"YAML_Frontmatter yaml-frontmatter", "YAML_Frontmatter yaml-frontmatter _", /* no extension */
"Z80 z80" "Z80 z80 .z80"
].map(function (line) { ].map(function (line) {
var kv = line.split(/\s/); var kv = line.split(/\s/);
return { return {
language: kv[0].replace(/_/g, ' '), language: kv[0].replace(/_/g, ' '),
mode: kv[1], mode: kv[1],
ext: kv[2], ext: kv[2] === '_' ? '' : kv[2],
}; };
}); });
Modes.extensionOf = function (mode) { Modes.extensionOf = function (mode) {
var ext = ''; var ext;
list.some(function (o) { list.some(function (o) {
if (o.mode !== mode) { return; } if (o.mode !== mode) { return; }
ext = o.ext || ''; ext = o.ext;
return true; return true;
}); });
return ext; return ext;

@ -2,74 +2,209 @@ define([
'jquery', 'jquery',
'/common/hyperscript.js', '/common/hyperscript.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-interface.js',
'/common/common-ui-elements.js', '/common/common-ui-elements.js',
'/common/common-constants.js',
'/customize/messages.js', '/customize/messages.js',
], function ($, h, Hash, UIElements, Messages) { '/bower_components/nthen/index.js'
], function ($, h, Hash, UI, UIElements, Constants, Messages, nThen) {
var handlers = {}; var handlers = {};
var defaultDismiss = function (common, data) {
return function (e) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
common.mailbox.dismiss(data, function (err) {
if (err) { return void console.error(err); }
});
};
};
// Friend request // Friend request
handlers['FRIEND_REQUEST'] = function (common, data, el) { handlers['FRIEND_REQUEST'] = function (common, data) {
var content = data.content; var content = data.content;
var msg = content.msg; var msg = content.msg;
// Display the notification
content.getFormatText = function () {
return Messages._getKey('friendRequest_notification', [msg.content.displayName || Messages.anonymous]);
};
// Check authenticity // Check authenticity
if (msg.author !== msg.content.curvePublic) { return; } if (msg.author !== msg.content.curvePublic) { return; }
common.addFriendRequest(data); // if not archived, add handlers
if (!content.archived) {
// Display the notification content.handler = function () {
$(el).find('.cp-notification-content p')
.html(Messages._getKey('friendRequest_notification', [msg.content.displayName || Messages.anonymous]));
$(el).find('.cp-notification-content').addClass("cp-clickable")
.click(function () {
UIElements.displayFriendRequestModal(common, data); UIElements.displayFriendRequestModal(common, data);
}); };
common.addFriendRequest(data);
}
}; };
handlers['FRIEND_REQUEST_ACCEPTED'] = function (common, data, el) { handlers['FRIEND_REQUEST_ACCEPTED'] = function (common, data) {
var content = data.content; var content = data.content;
var msg = content.msg; var msg = content.msg;
$(el).find('.cp-notification-content p') content.getFormatText = function () {
.html(Messages._getKey('friendRequest_accepted', [msg.content.name || Messages.anonymous])); return Messages._getKey('friendRequest_accepted', [msg.content.name || Messages.anonymous]);
$(el).find('.cp-notification-dismiss').css('display', 'flex'); };
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
}; };
handlers['FRIEND_REQUEST_DECLINED'] = function (common, data, el) { handlers['FRIEND_REQUEST_DECLINED'] = function (common, data) {
var content = data.content; var content = data.content;
var msg = content.msg; var msg = content.msg;
$(el).find('.cp-notification-content p') content.getFormatText = function () {
.html(Messages._getKey('friendRequest_declined', [msg.content.name || Messages.anonymous])); return Messages._getKey('friendRequest_declined', [msg.content.name || Messages.anonymous]);
$(el).find('.cp-notification-dismiss').css('display', 'flex'); };
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
}; };
// Share pad // Share pad
handlers['SHARE_PAD'] = function (common, data, el) { handlers['SHARE_PAD'] = function (common, data) {
var content = data.content; var content = data.content;
var msg = content.msg; var msg = content.msg;
var type = Hash.parsePadUrl(msg.content.href).type; var type = Hash.parsePadUrl(msg.content.href).type;
var key = type === 'drive' ? 'notification_folderShared' : var key = type === 'drive' ? 'notification_folderShared' :
(type === 'file' ? 'notification_fileShared' : (type === 'file' ? 'notification_fileShared' :
'notification_padShared'); 'notification_padShared');
$(el).find('.cp-notification-content p') content.getFormatText = function () {
.html(Messages._getKey(key, [msg.content.name || Messages.anonymous, msg.content.title])); return Messages._getKey(key, [msg.content.name || Messages.anonymous, msg.content.title]);
$(el).find('.cp-notification-content').addClass("cp-clickable") };
.click(function () { content.handler = function () {
var todo = function () {
common.openURL(msg.content.href); common.openURL(msg.content.href);
defaultDismiss(common, data)();
};
nThen(function (waitFor) {
if (msg.content.isTemplate) {
common.sessionStorage.put(Constants.newPadPathKey, ['template'], waitFor());
}
if (msg.content.password) {
common.sessionStorage.put('newPadPassword', msg.content.password, waitFor());
}
}).nThen(function () {
todo();
}); });
$(el).find('.cp-notification-dismiss').css('display', 'flex'); };
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
// New support message from the admins
handlers['SUPPORT_MESSAGE'] = function (common, data) {
var content = data.content;
content.getFormatText = function () {
return Messages.support_notification;
};
content.handler = function () {
common.openURL('/support/');
defaultDismiss(common, data)();
};
};
handlers['REQUEST_PAD_ACCESS'] = function (common, data) {
var content = data.content;
var msg = content.msg;
// Check authenticity
if (msg.author !== msg.content.user.curvePublic) { return; }
// Display the notification
content.getFormatText = function () {
return Messages._getKey('requestEdit_request', [msg.content.title, msg.content.user.displayName]);
};
// if not archived, add handlers
content.handler = function () {
var metadataMgr = common.getMetadataMgr();
var priv = metadataMgr.getPrivateData();
var link = h('a', {
href: '#'
}, Messages.requestEdit_viewPad);
var verified = h('p');
var $verified = $(verified);
if (priv.friends && priv.friends[msg.author]) {
$verified.addClass('cp-notifications-requestedit-verified');
var f = priv.friends[msg.author];
$verified.append(h('span.fa.fa-certificate'));
var $avatar = $(h('span.cp-avatar')).appendTo($verified);
$verified.append(h('p', Messages._getKey('requestEdit_fromFriend', [f.displayName])));
common.displayAvatar($avatar, f.avatar, f.displayName);
} else {
$verified.append(Messages._getKey('requestEdit_fromStranger', [msg.content.user.displayName]));
}
var div = h('div', [
UI.setHTML(h('p'), Messages._getKey('requestEdit_confirm', [msg.content.title, msg.content.user.displayName])),
verified,
link
]);
$(link).click(function (e) {
e.preventDefault();
e.stopPropagation();
common.openURL(msg.content.href);
});
UI.confirm(div, function (yes) {
if (!yes) { return; }
common.getSframeChannel().event('EV_GIVE_ACCESS', {
channel: msg.content.channel,
user: msg.content.user
});
defaultDismiss(common, data)();
}, {
ok: Messages.friendRequest_accept,
cancel: Messages.later
});
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
handlers['GIVE_PAD_ACCESS'] = function (common, data) {
var content = data.content;
var msg = content.msg;
// Check authenticity
if (msg.author !== msg.content.user.curvePublic) { return; }
if (!msg.content.href) { return; }
// Display the notification
content.getFormatText = function () {
return Messages._getKey('requestEdit_accepted', [msg.content.title, msg.content.user.displayName]);
};
// if not archived, add handlers
content.handler = function () {
common.openURL(msg.content.href);
defaultDismiss(common, data)();
};
}; };
return { return {
add: function (common, data, el) { add: function (common, data) {
var type = data.content.msg.type; var type = data.content.msg.type;
if (handlers[type]) { if (handlers[type]) {
handlers[type](common, data, el); handlers[type](common, data);
} else { // add getters to access simply some informations
$(el).find('.cp-notification-dismiss').css('display', 'flex'); data.content.isClickable = typeof data.content.handler === "function";
data.content.isDismissible = typeof data.content.dismissHandler === "function";
} }
}, },
remove: function (common, data) { remove: function (common, data) {

@ -44,6 +44,7 @@ body.cp-app-sheet, body.cp-app-oodoc, body.cp-app-ooslide {
height: 100%; height: 100%;
background-color: lightgrey; background-color: lightgrey;
display: flex; display: flex;
min-height: 0;
} }
#cp-app-oo-editor { #cp-app-oo-editor {
flex: 1; flex: 1;

@ -102,9 +102,12 @@ define([
Cryptpad.onlyoffice.onEvent.reg(function (obj) { Cryptpad.onlyoffice.onEvent.reg(function (obj) {
if (obj.ev === 'MESSAGE' && !/^cp\|/.test(obj.data)) { if (obj.ev === 'MESSAGE' && !/^cp\|/.test(obj.data)) {
try { try {
var validateKey = obj.data.validateKey || true;
var skipCheck = validateKey === true;
var msg = obj.data.msg;
obj.data = { obj.data = {
msg: JSON.parse(Utils.crypto.decrypt(obj.data, Utils.secret.keys.validateKey)), msg: JSON.parse(Utils.crypto.decrypt(msg, validateKey, skipCheck)),
hash: obj.data.slice(0,64) hash: msg.slice(0,64)
}; };
} catch (e) { } catch (e) {
console.error(e); console.error(e);

@ -8077,9 +8077,6 @@ jQuery.fn.extend({
prop = jQuery.extend( {}, prop ); prop = jQuery.extend( {}, prop );
function doAnimation() { function doAnimation() {
// XXX 'this' does not always have a nodeName when running the
// test suite
if ( optall.queue === false ) { if ( optall.queue === false ) {
jQuery._mark( this ); jQuery._mark( this );
} }

@ -478,7 +478,9 @@ define([
settings: store.proxy.settings, settings: store.proxy.settings,
thumbnails: disableThumbnails === false, thumbnails: disableThumbnails === false,
isDriveOwned: Boolean(Util.find(store, ['driveMetadata', 'owners'])), isDriveOwned: Boolean(Util.find(store, ['driveMetadata', 'owners'])),
pendingFriends: store.proxy.friends_pending || {} support: Util.find(store.proxy, ['mailboxes', 'support', 'channel']),
pendingFriends: store.proxy.friends_pending || {},
supportPrivateKey: Util.find(store.proxy, ['mailboxes', 'supportadmin', 'keys', 'curvePrivate'])
} }
}; };
cb(JSON.parse(JSON.stringify(metadata))); cb(JSON.parse(JSON.stringify(metadata)));
@ -1059,6 +1061,27 @@ define([
cb(res); cb(res);
}); });
}; };
Store.addAdminMailbox = function (clientId, data, cb) {
var priv = data;
var pub = Hash.getBoxPublicFromSecret(priv);
if (!priv || !pub) { return void cb({error: 'EINVAL'}); }
var channel = Hash.getChannelIdFromKey(pub);
var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {};
var box = mailboxes.supportadmin = {
channel: channel,
viewed: [],
lastKnownHash: '',
keys: {
curvePublic: pub,
curvePrivate: priv
}
};
Store.pinPads(null, [channel], function () {});
store.mailbox.open('supportadmin', box, function () {
console.log('ready');
});
onSync(cb);
};
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
/////////////////////// PAD ////////////////////////////////////// /////////////////////// PAD //////////////////////////////////////
@ -1067,6 +1090,7 @@ define([
var channels = Store.channels = store.channels = {}; var channels = Store.channels = store.channels = {};
Store.joinPad = function (clientId, data) { Store.joinPad = function (clientId, data) {
console.log('joining', data.channel);
var isNew = typeof channels[data.channel] === "undefined"; var isNew = typeof channels[data.channel] === "undefined";
var channel = channels[data.channel] = channels[data.channel] || { var channel = channels[data.channel] = channels[data.channel] || {
queue: [], queue: [],
@ -1220,6 +1244,97 @@ define([
channel.sendMessage(msg, clientId, cb); channel.sendMessage(msg, clientId, cb);
}; };
Store.requestPadAccess = function (clientId, data, cb) {
// Get owners from pad metadata
// Try to find an owner in our friend list
// Mailbox...
var channel = channels[data.channel];
if (!data.send && channel && (!channel.data || !channel.data.channel)) {
var i = 0;
var it = setInterval(function () {
if (channel.data && channel.data.channel) {
clearInterval(it);
Store.requestPadAccess(clientId, data, cb);
return;
}
if (i >= 300) { // One minute timeout
clearInterval(it);
}
i++;
}, 200);
return;
}
var fData = channel.data || {};
if (fData.owners) {
var friends = store.proxy.friends || {};
if (Object.keys(friends).length > 1) {
var owner;
fData.owners.some(function (edPublic) {
return Object.keys(friends).some(function (curve) {
if (curve === "me") { return; }
if (edPublic === friends[curve].edPublic &&
friends[curve].notifications) {
owner = friends[curve];
return true;
}
});
});
if (owner) {
if (data.send) {
var myData = Messaging.createData(store.proxy);
delete myData.channel;
store.mailbox.sendTo('REQUEST_PAD_ACCESS', {
channel: data.channel,
user: myData
}, {
channel: owner.notifications,
curvePublic: owner.curvePublic
}, function () {
cb({state: true});
});
return;
}
return void cb({state: true});
}
}
}
cb({sent: false});
};
Store.givePadAccess = function (clientId, data, cb) {
var edPublic = store.proxy.edPublic;
var channel = data.channel;
var res = store.manager.findChannel(channel);
if (!data.user || !data.user.notifications || !data.user.curvePublic) {
return void cb({error: 'EINVAL'});
}
var href, title;
if (!res.some(function (obj) {
if (obj.data &&
Array.isArray(obj.data.owners) && obj.data.owners.indexOf(edPublic) !== -1 &&
obj.data.href) {
href = obj.data.href;
title = obj.data.title;
return true;
}
})) { return void cb({error: 'ENOTFOUND'}); }
var myData = Messaging.createData(store.proxy);
delete myData.channel;
store.mailbox.sendTo("GIVE_PAD_ACCESS", {
channel: channel,
href: href,
title: title,
user: myData
}, {
channel: data.user.notifications,
curvePublic: data.user.curvePublic
});
cb();
};
// GET_FULL_HISTORY from sframe-common-outer // GET_FULL_HISTORY from sframe-common-outer
Store.getFullHistory = function (clientId, data, cb) { Store.getFullHistory = function (clientId, data, cb) {
var network = store.network; var network = store.network;
@ -1683,7 +1798,7 @@ define([
state: (2 + (version / 10)), state: (2 + (version / 10)),
progress: progress progress: progress
}); });
}); }, store);
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
postMessage(clientId, 'LOADING_DRIVE', { postMessage(clientId, 'LOADING_DRIVE', {
state: 3 state: 3

@ -1,6 +1,7 @@
define([ define([
'/common/common-messaging.js', '/common/common-messaging.js',
], function (Messaging) { '/common/common-hash.js',
], function (Messaging, Hash) {
var getRandomTimeout = function (ctx) { var getRandomTimeout = function (ctx) {
var lag = ctx.store.realtime.getLag().lag || 0; var lag = ctx.store.realtime.getLag().lag || 0;
@ -156,6 +157,108 @@ define([
cb(true); cb(true);
}; };
// Hide duplicates when receiving a SHARE_PAD notification:
// Keep only one notification per channel: the stronger and more recent one
var channels = {};
handlers['SHARE_PAD'] = function (ctx, box, data, cb) {
var msg = data.msg;
var hash = data.hash;
var content = msg.content;
// content.name, content.title, content.href, content.password
var channel = Hash.hrefToHexChannelId(content.href, content.password);
var parsed = Hash.parsePadUrl(content.href);
var mode = parsed.hashData && parsed.hashData.mode || 'n/a';
var old = channels[channel];
var toRemove;
if (old) {
// New hash is weaker, ignore
if (old.mode === 'edit' && mode === 'view') {
return void cb(true);
}
// New hash is not weaker, clear the old one
toRemove = old.data;
}
// Update the data
channels[channel] = {
mode: mode,
data: {
type: box.type,
hash: hash
}
};
cb(false, toRemove);
};
removeHandlers['SHARE_PAD'] = function (ctx, box, data, hash) {
var content = data.content;
var channel = Hash.hrefToHexChannelId(content.href, content.password);
var old = channels[channel];
if (old && old.data && old.data.hash === hash) {
delete channels[channel];
}
};
// Hide duplicates when receiving a SUPPORT_MESSAGE notification
var supportMessage = false;
handlers['SUPPORT_MESSAGE'] = function (ctx, box, data, cb) {
if (supportMessage) { return void cb(true); }
supportMessage = true;
cb();
};
// Incoming edit rights request: add data before sending it to inner
handlers['REQUEST_PAD_ACCESS'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
var channel = content.channel;
var res = ctx.store.manager.findChannel(channel);
if (!res.length) { return void cb(true); }
var edPublic = ctx.store.proxy.edPublic;
var title, href;
if (!res.some(function (obj) {
if (obj.data &&
Array.isArray(obj.data.owners) && obj.data.owners.indexOf(edPublic) !== -1 &&
obj.data.href) {
href = obj.data.href;
title = obj.data.filename || obj.data.title;
return true;
}
})) { return void cb(true); }
content.title = title;
content.href = href;
cb(false);
};
handlers['GIVE_PAD_ACCESS'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
var channel = content.channel;
var res = ctx.store.manager.findChannel(channel);
var title;
res.forEach(function (obj) {
if (obj.data && !obj.data.href) {
if (!title) { title = obj.data.filename || obj.data.title; }
obj.data.href = content.href;
}
});
content.title = title || content.title;
cb(false);
};
return { return {
add: function (ctx, box, data, cb) { add: function (ctx, box, data, cb) {
/** /**

@ -10,6 +10,8 @@ define([
var TYPES = [ var TYPES = [
'notifications', 'notifications',
'supportadmin',
'support'
]; ];
var BLOCKING_TYPES = [ var BLOCKING_TYPES = [
]; ];
@ -25,6 +27,16 @@ define([
if (res.error) { console.error(res); } if (res.error) { console.error(res); }
}); });
} }
if (!mailboxes['support']) {
mailboxes.support = {
channel: Hash.createChannelId(),
lastKnownHash: '',
viewed: []
};
ctx.pinPads([mailboxes.support.channel], function (res) {
if (res.error) { console.error(res); }
});
}
}; };
/* /*
@ -76,15 +88,32 @@ proxy.mailboxes = {
var crypto = Crypto.Mailbox.createEncryptor(keys); var crypto = Crypto.Mailbox.createEncryptor(keys);
var network = ctx.store.network; var network = ctx.store.network;
var ciphertext = crypto.encrypt(JSON.stringify({ var text = JSON.stringify({
type: type, type: type,
content: msg content: msg
}), user.curvePublic); });
var ciphertext = crypto.encrypt(text, user.curvePublic);
network.join(user.channel).then(function (wc) { network.join(user.channel).then(function (wc) {
wc.bcast(ciphertext).then(function () { wc.bcast(ciphertext).then(function () {
cb(); cb();
wc.leave();
// If we've just sent a message to one of our mailboxes, we have to trigger the handler manually
// (the server won't send back our message to us)
// If it isn't one of our mailboxes, we can close it now
var box;
if (Object.keys(ctx.boxes).some(function (t) {
var _box = ctx.boxes[t];
if (_box.channel === user.channel) {
box = _box;
return true;
}
})) {
var hash = ciphertext.slice(0, 64);
box.onMessage(text, null, null, null, hash, user.curvePublic);
} else {
wc.leave();
}
}); });
}, function (err) { }, function (err) {
cb({error: err}); cb({error: err});
@ -157,6 +186,8 @@ proxy.mailboxes = {
var openChannel = function (ctx, type, m, onReady) { var openChannel = function (ctx, type, m, onReady) {
var box = ctx.boxes[type] = { var box = ctx.boxes[type] = {
channel: m.channel,
type: type,
queue: [], // Store the messages to send when the channel is ready queue: [], // Store the messages to send when the channel is ready
history: [], // All the hashes loaded from the server in corretc order history: [], // All the hashes loaded from the server in corretc order
content: {}, // Content of the messages that should be displayed content: {}, // Content of the messages that should be displayed
@ -173,9 +204,10 @@ proxy.mailboxes = {
if (!Crypto.Mailbox) { if (!Crypto.Mailbox) {
return void console.error("chainpad-crypto is outdated and doesn't support mailboxes."); return void console.error("chainpad-crypto is outdated and doesn't support mailboxes.");
} }
var keys = getMyKeys(ctx); var keys = m.keys || getMyKeys(ctx);
if (!keys) { return void console.error("missing asymmetric encryption keys"); } if (!keys) { return void console.error("missing asymmetric encryption keys"); }
var crypto = Crypto.Mailbox.createEncryptor(keys); var crypto = Crypto.Mailbox.createEncryptor(keys);
box.encryptor = crypto;
var cfg = { var cfg = {
network: ctx.store.network, network: ctx.store.network,
channel: m.channel, channel: m.channel,
@ -213,8 +245,11 @@ proxy.mailboxes = {
}); });
box.queue = []; box.queue = [];
}; };
cfg.onMessage = function (msg, user, vKey, isCp, hash, author) { var lastReceivedHash; // Don't send a duplicate of the last known hash on reconnect
box.onMessage = cfg.onMessage = function (msg, user, vKey, isCp, hash, author) {
if (hash === m.lastKnownHash) { return; } if (hash === m.lastKnownHash) { return; }
if (hash === lastReceivedHash) { return; }
lastReceivedHash = hash;
try { try {
msg = JSON.parse(msg); msg = JSON.parse(msg);
} catch (e) { } catch (e) {
@ -228,8 +263,8 @@ proxy.mailboxes = {
msg: msg, msg: msg,
hash: hash hash: hash
}; };
Handlers.add(ctx, box, message, function (toDismiss) { Handlers.add(ctx, box, message, function (dismissed, toDismiss) {
if (toDismiss) { if (dismissed) { // This message should be removed
dismiss(ctx, { dismiss(ctx, {
type: type, type: type,
hash: hash hash: hash
@ -238,6 +273,11 @@ proxy.mailboxes = {
}); });
return; return;
} }
if (toDismiss) { // List of other messages to remove
dismiss(ctx, toDismiss, '', function () {
console.log('Notification handled automatically');
});
}
box.content[hash] = msg; box.content[hash] = msg;
showMessage(ctx, type, message); showMessage(ctx, type, message);
}); });
@ -293,6 +333,63 @@ proxy.mailboxes = {
CpNetflux.start(cfg); CpNetflux.start(cfg);
}; };
var initializeHistory = function (ctx) {
var network = ctx.store.network;
network.on('message', function (msg, sender) {
if (sender !== network.historyKeeper) { return; }
var parsed = JSON.parse(msg);
if (!/HISTORY_RANGE/.test(parsed[0])) { return; }
var txid = parsed[1];
var req = ctx.req[txid];
if (!req) { return; }
var type = parsed[0];
var _msg = parsed[2];
var box = req.box;
if (type === 'HISTORY_RANGE') {
if (!Array.isArray(_msg)) { return; }
var message;
try {
var decrypted = box.encryptor.decrypt(_msg[4]);
message = JSON.parse(decrypted.content);
} catch (e) {
console.log(e);
}
ctx.emit('HISTORY', {
txid: txid,
time: _msg[5],
message: message,
hash: _msg[4].slice(0,64)
}, [req.cId]);
} else if (type === 'HISTORY_RANGE_END') {
ctx.emit('HISTORY', {
txid: txid,
complete: true
}, [req.cId]);
delete ctx.req[txid];
}
});
};
var loadHistory = function (ctx, clientId, data, cb) {
var box = ctx.boxes[data.type];
if (!box) { return void cb({error: 'ENOENT'}); }
var msg = [ 'GET_HISTORY_RANGE', box.channel, {
from: data.lastKnownHash,
count: data.count,
txid: data.txid
}
];
ctx.req[data.txid] = {
cId: clientId,
box: box
};
var network = ctx.store.network;
network.sendto(network.historyKeeper, JSON.stringify(msg)).then(function () {
}, function (err) {
console.error(err);
});
};
var subscribe = function (ctx, data, cId, cb) { var subscribe = function (ctx, data, cId, cb) {
// Get existing notifications // Get existing notifications
@ -327,12 +424,14 @@ proxy.mailboxes = {
updateMetadata: cfg.updateMetadata, updateMetadata: cfg.updateMetadata,
emit: emit, emit: emit,
clients: [], clients: [],
boxes: {} boxes: {},
req: {}
}; };
var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {}; var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {};
initializeMailboxes(ctx, mailboxes); initializeMailboxes(ctx, mailboxes);
initializeHistory(ctx);
Object.keys(mailboxes).forEach(function (key) { Object.keys(mailboxes).forEach(function (key) {
if (TYPES.indexOf(key) === -1) { return; } if (TYPES.indexOf(key) === -1) { return; }
@ -359,6 +458,11 @@ proxy.mailboxes = {
}); });
}; };
mailbox.open = function (key, m, cb) {
if (TYPES.indexOf(key) === -1) { return; }
openChannel(ctx, key, m, cb);
};
mailbox.dismiss = function (data, cb) { mailbox.dismiss = function (data, cb) {
dismiss(ctx, data, '', cb); dismiss(ctx, data, '', cb);
}; };
@ -382,6 +486,9 @@ proxy.mailboxes = {
if (cmd === 'SENDTO') { if (cmd === 'SENDTO') {
return void sendTo(ctx, data.type, data.msg, data.user, cb); return void sendTo(ctx, data.type, data.msg, data.user, cb);
} }
if (cmd === 'LOAD_HISTORY') {
return void loadHistory(ctx, clientId, data, cb);
}
}; };
return mailbox; return mailbox;

@ -26,7 +26,10 @@ define([
if (!c.id) { c.id = chan.wc.myID + '-' + client; } if (!c.id) { c.id = chan.wc.myID + '-' + client; }
chan.history.forEach(function (msg) { chan.history.forEach(function (msg) {
ctx.emit('MESSAGE', msg, [client]); ctx.emit('MESSAGE', {
msg: msg,
validateKey: chan.validateKey
}, [client]);
}); });
// ==> And push the new tab to the list // ==> And push the new tab to the list
@ -37,7 +40,8 @@ define([
var onOpen = function (wc) { var onOpen = function (wc) {
ctx.channels[channel] = ctx.channels[channel] || { ctx.channels[channel] = ctx.channels[channel] || {
history: [] history: [],
validateKey: obj.validateKey
}; };
chan = ctx.channels[channel]; chan = ctx.channels[channel];
@ -61,7 +65,10 @@ define([
}); });
wc.on('message', function (msg) { wc.on('message', function (msg) {
chan.history.push(msg); chan.history.push(msg);
ctx.emit('MESSAGE', msg, chan.clients); ctx.emit('MESSAGE', {
msg: msg,
validateKey: chan.validateKey
}, chan.clients);
}); });
chan.wc = wc; chan.wc = wc;
@ -101,6 +108,7 @@ define([
}; };
network.on('message', function (msg, sender) { network.on('message', function (msg, sender) {
if (!ctx.channels[channel]) { return; }
var hk = network.historyKeeper; var hk = network.historyKeeper;
if (sender !== hk) { return; } if (sender !== hk) { return; }
@ -115,7 +123,12 @@ define([
// Keep only metadata messages for the current channel // Keep only metadata messages for the current channel
if (parsed.channel && parsed.channel !== channel) { return; } if (parsed.channel && parsed.channel !== channel) { return; }
// Ignore the metadata message // Ignore the metadata message
if (parsed.validateKey && parsed.channel) { return; } if (parsed.validateKey && parsed.channel) {
if (!chan.validateKey) {
chan.validateKey = parsed.validateKey;
}
return;
}
// End of history: emit READY // End of history: emit READY
if (parsed.state && parsed.state === 1 && parsed.channel) { if (parsed.state && parsed.state === 1 && parsed.channel) {
ctx.emit('READY', '', chan.clients); ctx.emit('READY', '', chan.clients);
@ -132,7 +145,9 @@ define([
if (hash === chan.lastKnownHash || hash === chan.lastCpHash) { return; } if (hash === chan.lastKnownHash || hash === chan.lastCpHash) { return; }
chan.lastKnownHash = hash; chan.lastKnownHash = hash;
ctx.emit('MESSAGE', msg, chan.clients); ctx.emit('MESSAGE', {
msg: msg,
}, chan.clients);
chan.history.push(msg); chan.history.push(msg);
}); });
@ -176,7 +191,9 @@ define([
return void chan.sendMsg(data.isCp, cb); return void chan.sendMsg(data.isCp, cb);
} }
chan.sendMsg(data.msg, cb); chan.sendMsg(data.msg, cb);
ctx.emit('MESSAGE', data.msg, chan.clients.filter(function (cl) { ctx.emit('MESSAGE', {
msg: data.msg
}, chan.clients.filter(function (cl) {
return cl !== clientId; return cl !== clientId;
})); }));
}; };

@ -78,12 +78,15 @@ define([
GET_FULL_HISTORY: Store.getFullHistory, GET_FULL_HISTORY: Store.getFullHistory,
GET_HISTORY_RANGE: Store.getHistoryRange, GET_HISTORY_RANGE: Store.getHistoryRange,
IS_NEW_CHANNEL: Store.isNewChannel, IS_NEW_CHANNEL: Store.isNewChannel,
REQUEST_PAD_ACCESS: Store.requestPadAccess,
GIVE_PAD_ACCESS: Store.givePadAccess,
// Drive // Drive
DRIVE_USEROBJECT: Store.userObjectCommand, DRIVE_USEROBJECT: Store.userObjectCommand,
// Settings, // Settings,
DELETE_ACCOUNT: Store.deleteAccount, DELETE_ACCOUNT: Store.deleteAccount,
// Admin // Admin
ADMIN_RPC: Store.adminRpc, ADMIN_RPC: Store.adminRpc,
ADMIN_ADD_MAILBOX: Store.addAdminMailbox,
}; };
Rpc.query = function (cmd, data, cb) { Rpc.query = function (cmd, data, cb) {

@ -506,8 +506,14 @@ define([
var fixRoot = function (elem) { var fixRoot = function (elem) {
if (typeof(files[ROOT]) !== "object") { debug("ROOT was not an object"); files[ROOT] = {}; } if (typeof(files[ROOT]) !== "object") { debug("ROOT was not an object"); files[ROOT] = {}; }
var element = elem || files[ROOT]; var element = elem || files[ROOT];
if (!element) { return console.error("Invalid element in root"); }
var nbMetadataFolders = 0; var nbMetadataFolders = 0;
for (var el in element) { for (var el in element) {
if (element[el] === null) {
console.error('element[%s] is null', el);
delete element[el];
continue;
}
if (exp.isFolderData(element[el])) { if (exp.isFolderData(element[el])) {
if (nbMetadataFolders !== 0) { if (nbMetadataFolders !== 0) {
debug("Multiple metadata files in folder"); debug("Multiple metadata files in folder");
@ -625,6 +631,11 @@ define([
var root = exp.find([ROOT]); var root = exp.find([ROOT]);
var toClean = []; var toClean = [];
for (var id in fd) { for (var id in fd) {
if (String(id) !== String(Number(id))) {
debug("Invalid file ID in filesData.", id);
toClean.push(id);
continue;
}
id = Number(id); id = Number(id);
var el = fd[id]; var el = fd[id];

@ -252,6 +252,9 @@ define([
var obj = Env.folders[el].proxy.metadata || {}; var obj = Env.folders[el].proxy.metadata || {};
if (obj) { key = obj.title; } if (obj) { key = obj.title; }
} else { } else {
try {
el = JSON.parse(JSON.stringify(el));
} catch (e) { return undefined; }
userObject.getFilesRecursively(el, files); userObject.getFilesRecursively(el, files);
} }
@ -342,7 +345,7 @@ define([
}); });
// Remove the elements from the old location (without unpinning) // Remove the elements from the old location (without unpinning)
Env.user.userObject.delete(resolved.main, waitFor()); Env.user.userObject.delete(resolved.main, waitFor()); // FIXME waitFor() is called synchronously
} }
} }
} }
@ -369,7 +372,7 @@ define([
if (copy) { return; } if (copy) { return; }
// Remove the elements from the old location (without unpinning) // Remove the elements from the old location (without unpinning)
uoFrom.delete(paths, waitFor()); uoFrom.delete(paths, waitFor()); // FIXME waitFor() is called synchronously
} }
}); });
} }
@ -707,6 +710,7 @@ define([
if (type === 'expirable') { if (type === 'expirable') {
return function (fileId) { return function (fileId) {
var data = userObject.getFileData(fileId); var data = userObject.getFileData(fileId);
if (!data) { return; }
// Don't push duplicates // Don't push duplicates
if (result.indexOf(data.channel) !== -1) { return; } if (result.indexOf(data.channel) !== -1) { return; }
// Return pads owned by someone else or expired by time // Return pads owned by someone else or expired by time
@ -718,6 +722,7 @@ define([
if (type === 'owned') { if (type === 'owned') {
return function (fileId) { return function (fileId) {
var data = userObject.getFileData(fileId); var data = userObject.getFileData(fileId);
if (!data) { return; }
// Don't push duplicates // Don't push duplicates
if (result.indexOf(data.channel) !== -1) { return; } if (result.indexOf(data.channel) !== -1) { return; }
// Return owned pads // Return owned pads
@ -729,6 +734,7 @@ define([
if (type === "pin") { if (type === "pin") {
return function (fileId) { return function (fileId) {
var data = userObject.getFileData(fileId); var data = userObject.getFileData(fileId);
if (!data) { return; }
// Don't pin pads owned by someone else // Don't pin pads owned by someone else
if (_ownedByOther(Env, data.owners)) { return; } if (_ownedByOther(Env, data.owners)) { return; }
// Don't push duplicates // Don't push duplicates

@ -401,7 +401,7 @@ define([
var ext = (typeof(extension) === 'function') ? extension() : extension; var ext = (typeof(extension) === 'function') ? extension() : extension;
var suggestion = title.suggestTitle('cryptpad-document'); var suggestion = title.suggestTitle('cryptpad-document');
UI.prompt(Messages.exportPrompt, UI.prompt(Messages.exportPrompt,
Util.fixFileName(suggestion) + '.' + ext, function (filename) Util.fixFileName(suggestion) + ext, function (filename)
{ {
if (!(typeof(filename) === 'string' && filename)) { return; } if (!(typeof(filename) === 'string' && filename)) { return; }
if (async) { if (async) {
@ -454,7 +454,7 @@ define([
return; return;
} }
if (!mediaTagEmbedder) { console.log('mediaTagEmbedder missing'); return; } if (!mediaTagEmbedder) { console.log('mediaTagEmbedder missing'); return; }
if (data.type !== 'file') { console.log('unhandled embed type ' + data.type); return; } if (data.type !== 'file') { console.log('unhandled embed type ' + data.type); return; }
var privateDat = cpNfInner.metadataMgr.getPrivateData(); var privateDat = cpNfInner.metadataMgr.getPrivateData();
var origin = privateDat.fileHost || privateDat.origin; var origin = privateDat.fileHost || privateDat.origin;
var src = data.src = origin + data.src; var src = data.src = origin + data.src;
@ -603,6 +603,7 @@ define([
'newpad', 'newpad',
'share', 'share',
'limit', 'limit',
'request',
'unpinnedWarning', 'unpinnedWarning',
'notifications' 'notifications'
], ],

@ -39,7 +39,8 @@ define([
}; };
module.getContentExtension = function (mode) { module.getContentExtension = function (mode) {
return (Modes.extensionOf(mode) || '.txt').slice(1); var ext = Modes.extensionOf(mode);
return ext !== undefined ? ext : '.txt';
}; };
module.fileExporter = function (content) { module.fileExporter = function (content) {
return new Blob([ content ], { type: 'text/plain;charset=utf-8' }); return new Blob([ content ], { type: 'text/plain;charset=utf-8' });
@ -98,9 +99,17 @@ define([
// lines beginning with a hash are potentially valuable // lines beginning with a hash are potentially valuable
// works for markdown, python, bash, etc. // works for markdown, python, bash, etc.
var hash = /^#+(.*?)$/; var hash = /^#+(.*?)$/;
var hashAndLink = /^#+\s*\[(.*?)\]\(.*\)\s*$/;
if (hash.test(line)) { if (hash.test(line)) {
// test for link inside the title, and set text just to the name of the link
if (hashAndLink.test(line)) {
line.replace(hashAndLink, function (a, one) {
text = Util.stripTags(one);
});
return true;
}
line.replace(hash, function (a, one) { line.replace(hash, function (a, one) {
text = one; text = Util.stripTags(one);
}); });
return true; return true;
} }
@ -231,7 +240,7 @@ define([
}; };
var $block = exp.$language = UIElements.createDropdown(dropdownConfig); var $block = exp.$language = UIElements.createDropdown(dropdownConfig);
$block.find('button').attr('title', Messages.languageButtonTitle); $block.find('button').attr('title', Messages.languageButtonTitle);
var isHovering = false; var isHovering = false;
var $aLanguages = $block.find('a'); var $aLanguages = $block.find('a');
$aLanguages.mouseenter(function () { $aLanguages.mouseenter(function () {
@ -304,7 +313,7 @@ define([
setTheme(theme, $block); setTheme(theme, $block);
Common.setAttribute(themeKey, theme); Common.setAttribute(themeKey, theme);
}); });
if ($drawer) { $drawer.append($block); } if ($drawer) { $drawer.append($block); }
if (cb) { cb(); } if (cb) { cb(); }
}; };
@ -323,7 +332,7 @@ define([
var mode; var mode;
if (!mime) { if (!mime) {
var ext = /.+\.([^.]+)$/.exec(file.name); var ext = /.+\.([^.]+)$/.exec(file.name);
if (ext[1]) { if (ext && ext[1]) {
mode = CMeditor.findModeByExtension(ext[1]); mode = CMeditor.findModeByExtension(ext[1]);
mode = mode && mode.mode || null; mode = mode && mode.mode || null;
} }
@ -339,7 +348,8 @@ define([
exp.setMode('text'); exp.setMode('text');
$toolbarContainer.find('#language-mode').val('text'); $toolbarContainer.find('#language-mode').val('text');
} }
return { content: content }; // return the mode so that the code editor can decide how to display the new content
return { content: content, mode: mode };
}; };
exp.setValueAndCursor = function (oldDoc, remoteDoc) { exp.setValueAndCursor = function (oldDoc, remoteDoc) {
@ -385,21 +395,32 @@ define([
exp.mkIndentSettings = function (metadataMgr) { exp.mkIndentSettings = function (metadataMgr) {
var setIndentation = function (units, useTabs, fontSize, spellcheck) { var setIndentation = function (units, useTabs, fontSize, spellcheck) {
if (typeof(units) !== 'number') { return; } if (typeof(units) !== 'number') { return; }
var doc = editor.getDoc();
editor.setOption('indentUnit', units); editor.setOption('indentUnit', units);
editor.setOption('tabSize', units); editor.setOption('tabSize', units);
editor.setOption('indentWithTabs', useTabs); editor.setOption('indentWithTabs', useTabs);
editor.setOption('spellcheck', spellcheck); editor.setOption('spellcheck', spellcheck);
if (!useTabs) { editor.setOption("extraKeys", {
editor.setOption("extraKeys", { Tab: function() {
Tab: function() { if (doc.somethingSelected()) {
editor.replaceSelection(Array(units + 1).join(" ")); editor.execCommand("indentMore");
} }
}); else {
} else { if (!useTabs) { editor.execCommand("insertSoftTab"); }
editor.setOption("extraKeys", { else { editor.execCommand("insertTab"); }
Tab: undefined, }
}); },
} "Shift-Tab": function () {
editor.execCommand("indentLess");
},
"Backspace": function () {
var cursor = doc.getCursor();
var line = doc.getLine(cursor.line);
if (line.substring(0, cursor.ch).trim() === "") { editor.execCommand("indentLess"); }
else { editor.execCommand("delCharBefore"); }
},
});
$('.CodeMirror').css('font-size', fontSize+'px'); $('.CodeMirror').css('font-size', fontSize+'px');
}; };

@ -47,30 +47,29 @@ define([
var formatData = function (data) { var formatData = function (data) {
return JSON.stringify(data.content.msg.content); return JSON.stringify(data.content.msg.content);
}; };
var createElement = function (data) { var createElement = mailbox.createElement = function (data) {
var notif; var notif;
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();
mailbox.dismiss(data, function (err) {
if (err) { return void console.error(err); }
/*if (notif && notif.parentNode) {
try {
notif.parentNode.removeChild(notif);
} catch (e) { console.error(e); }
}*/
});
});
notif = h('div.cp-notification', { notif = h('div.cp-notification', {
'data-hash': data.content.hash 'data-hash': data.content.hash
}, [ }, [h('div.cp-notification-content', h('p', formatData(data)))]);
h('div.cp-notification-content', h('p', formatData(data))),
dismiss if (data.content.getFormatText) {
]); $(notif).find('.cp-notification-content p').html(data.content.getFormatText());
}
if (data.content.isClickable) {
$(notif).find('.cp-notification-content').addClass("cp-clickable")
.click(data.content.handler);
}
if (data.content.isDismissible) {
var dismissIcon = h('span.fa.fa-times');
var dismiss = h('div.cp-notification-dismiss', {
title: Messages.notifications_dismiss
}, dismissIcon);
$(dismiss).addClass("cp-clickable")
.click(data.content.dismissHandler);
$(notif).append(dismiss);
}
return notif; return notif;
}; };
@ -80,7 +79,7 @@ define([
onViewedHandlers.push(function (data) { onViewedHandlers.push(function (data) {
var hash = data.hash.replace(/"/g, '\\\"'); var hash = data.hash.replace(/"/g, '\\\"');
var $notif = $('.cp-notification[data-hash="'+hash+'"]'); var $notif = $('.cp-notification[data-hash="'+hash+'"]:not(.cp-app-notification-archived)');
if ($notif.length) { if ($notif.length) {
$notif.remove(); $notif.remove();
} }
@ -90,8 +89,11 @@ define([
var pushMessage = function (data, handler) { var pushMessage = function (data, handler) {
var todo = function (f) { var todo = function (f) {
try { try {
var el = createElement(data); var el;
Notifications.add(Common, data, el); if (data.type === 'notifications') {
Notifications.add(Common, data);
el = createElement(data);
}
f(data, el); f(data, el);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -108,7 +110,9 @@ define([
onViewedHandlers.forEach(function (f) { onViewedHandlers.forEach(function (f) {
try { try {
f(data); f(data);
Notifications.remove(Common, data); if (data.type === 'notifications') {
Notifications.remove(Common, data);
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@ -118,7 +122,7 @@ define([
var onMessage = function (data) { var onMessage = function (data) {
// data = { type: 'type', content: {msg: 'msg', hash: 'hash'} } // data = { type: 'type', content: {msg: 'msg', hash: 'hash'} }
console.log(data.content); console.log(data.type, data.content);
pushMessage(data); pushMessage(data);
if (!history[data.type]) { history[data.type] = []; } if (!history[data.type]) { history[data.type] = []; }
history[data.type].push(data.content); history[data.type].push(data.content);
@ -139,18 +143,25 @@ define([
var subscribed = false; var subscribed = false;
// Get all existing notifications + the new ones when they come // Get all existing notifications + the new ones when they come
mailbox.subscribe = function (cfg) { mailbox.subscribe = function (types, cfg) {
if (!subscribed) { if (!subscribed) {
execCommand('SUBSCRIBE', null, function () {}); execCommand('SUBSCRIBE', null, function () {});
subscribed = true; subscribed = true;
} }
if (typeof(cfg.onViewed) === "function") { if (typeof(cfg.onViewed) === "function") {
onViewedHandlers.push(cfg.onViewed); onViewedHandlers.push(function (data) {
if (types.indexOf(data.type) === -1) { return; }
cfg.onViewed(data);
});
} }
if (typeof(cfg.onMessage) === "function") { if (typeof(cfg.onMessage) === "function") {
onMessageHandlers.push(cfg.onMessage); onMessageHandlers.push(function (data, el) {
if (types.indexOf(data.type) === -1) { return; }
cfg.onMessage(data, el);
});
} }
Object.keys(history).forEach(function (type) { Object.keys(history).forEach(function (type) {
if (types.indexOf(type) === -1) { return; }
history[type].forEach(function (data) { history[type].forEach(function (data) {
pushMessage({ pushMessage({
type: type, type: type,
@ -160,6 +171,52 @@ define([
}); });
}; };
var historyState = false;
var onHistory = function () {};
mailbox.getMoreHistory = function (type, count, lastKnownHash, cb) {
if (historyState) { return void cb("ALREADY_CALLED"); }
historyState = true;
var txid = Util.uid();
execCommand('LOAD_HISTORY', {
type: type,
count: lastKnownHash ? count + 1 : count,
txid: txid,
lastKnownHash: lastKnownHash
}, function (err, obj) {
if (obj && obj.error) { console.error(obj.error); }
});
var messages = [];
onHistory = function (data) {
if (data.txid !== txid) { return; }
if (data.complete) {
onHistory = function () {};
var end = messages.length < count;
cb(null, messages, end);
historyState = false;
return;
}
if (data.hash !== lastKnownHash) {
messages.push({
type: type,
content: {
msg: data.message,
time: data.time,
hash: data.hash
}
});
}
};
};
mailbox.getNotificationsHistory = function (type, count, lastKnownHash, cb) {
mailbox.getMoreHistory(type, count, lastKnownHash, function (err, messages, end) {
if (!Array.isArray(messages)) { return void cb(err); }
messages.forEach(function (data) {
data.content.archived = true;
Notifications.add(Common, data);
});
cb(err, messages, end);
});
};
// CHANNEL WITH WORKER // CHANNEL WITH WORKER
@ -167,6 +224,9 @@ define([
// obj = { ev: 'type', data: obj } // obj = { ev: 'type', data: obj }
var ev = obj.ev; var ev = obj.ev;
var data = obj.data; var data = obj.data;
if (ev === 'HISTORY') {
return void onHistory(data);
}
if (ev === 'MESSAGE') { if (ev === 'MESSAGE') {
return void onMessage(data); return void onMessage(data);
} }

@ -223,6 +223,11 @@ define([
sframeChan.event("EV_PAD_PASSWORD"); sframeChan.event("EV_PAD_PASSWORD");
}; };
if (!val && sessionStorage.newPadPassword) {
val = sessionStorage.newPadPassword;
delete sessionStorage.newPadPassword;
}
if (val) { if (val) {
password = val; password = val;
Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) { Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) {
@ -267,23 +272,25 @@ define([
var parsed = Utils.Hash.parsePadUrl(window.location.href); var parsed = Utils.Hash.parsePadUrl(window.location.href);
if (!parsed.type) { throw new Error(); } if (!parsed.type) { throw new Error(); }
var defaultTitle = Utils.Hash.getDefaultName(parsed); var defaultTitle = Utils.Hash.getDefaultName(parsed);
var edPublic; var edPublic, isTemplate;
var forceCreationScreen = cfg.useCreationScreen && var forceCreationScreen = cfg.useCreationScreen &&
sessionStorage[Utils.Constants.displayPadCreationScreen]; sessionStorage[Utils.Constants.displayPadCreationScreen];
delete sessionStorage[Utils.Constants.displayPadCreationScreen]; delete sessionStorage[Utils.Constants.displayPadCreationScreen];
var updateMeta = function () { var updateMeta = function () {
//console.log('EV_METADATA_UPDATE'); //console.log('EV_METADATA_UPDATE');
var metaObj, isTemplate; var metaObj;
nThen(function (waitFor) { nThen(function (waitFor) {
Cryptpad.getMetadata(waitFor(function (err, m) { Cryptpad.getMetadata(waitFor(function (err, m) {
if (err) { console.log(err); } if (err) { console.log(err); }
metaObj = m; metaObj = m;
edPublic = metaObj.priv.edPublic; // needed to create an owned pad edPublic = metaObj.priv.edPublic; // needed to create an owned pad
})); }));
Cryptpad.isTemplate(window.location.href, waitFor(function (err, t) { if (typeof(isTemplate) === "undefined") {
if (err) { console.log(err); } Cryptpad.isTemplate(window.location.href, waitFor(function (err, t) {
isTemplate = t; if (err) { console.log(err); }
})); isTemplate = t;
}));
}
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {
metaObj.doc = { metaObj.doc = {
defaultTitle: defaultTitle, defaultTitle: defaultTitle,
@ -730,6 +737,7 @@ define([
var initShareModal = function (cfg) { var initShareModal = function (cfg) {
cfg.hashes = hashes; cfg.hashes = hashes;
cfg.password = password; cfg.password = password;
cfg.isTemplate = isTemplate;
// cfg.hidden means pre-loading the filepicker while keeping it hidden. // cfg.hidden means pre-loading the filepicker while keeping it hidden.
// if cfg.hidden is true and the iframe already exists, do nothing // if cfg.hidden is true and the iframe already exists, do nothing
if (!ShareModal.$iframe) { if (!ShareModal.$iframe) {
@ -936,6 +944,19 @@ define([
sframeChan.event('EV_WORKER_TIMEOUT'); sframeChan.event('EV_WORKER_TIMEOUT');
}); });
sframeChan.on('EV_GIVE_ACCESS', function (data, cb) {
Cryptpad.padRpc.giveAccess(data, cb);
});
sframeChan.on('Q_REQUEST_ACCESS', function (data, cb) {
if (readOnly && hashes.editHash) {
return void cb({error: 'ALREADYKNOWN'});
}
Cryptpad.padRpc.requestAccess({
send: data,
channel: secret.channel
}, cb);
});
if (cfg.messaging) { if (cfg.messaging) {
Notifier.getPermission(); Notifier.getPermission();

@ -79,7 +79,8 @@ define([
return { return {
updateTitle: exp.updateTitle, updateTitle: exp.updateTitle,
suggestName: suggestTitle, suggestName: suggestTitle,
defaultName: exp.defaultTitle defaultName: exp.defaultTitle,
getTitle: exp.getTitle
}; };
}; };

@ -534,8 +534,11 @@ MessengerUI, Messages) {
hidden: true hidden: true
}); });
$shareBlock.click(function () { $shareBlock.click(function () {
var title = (config.title && config.title.getTitle && config.title.getTitle())
|| (config.title && config.title.defaultName)
|| "";
Common.getSframeChannel().event('EV_SHARE_OPEN', { Common.getSframeChannel().event('EV_SHARE_OPEN', {
title: Common.getMetadataMgr().getMetadata().title title: title
}); });
}); });
@ -559,7 +562,10 @@ MessengerUI, Messages) {
file: true file: true
}); });
$shareBlock.click(function () { $shareBlock.click(function () {
var title = (config.title && config.title.getTitle && config.title.getTitle())
|| "";
Common.getSframeChannel().event('EV_SHARE_OPEN', { Common.getSframeChannel().event('EV_SHARE_OPEN', {
title: title,
file: true file: true
}); });
}); });
@ -568,6 +574,48 @@ MessengerUI, Messages) {
return $shareBlock; return $shareBlock;
}; };
var createRequest = function (toolbar, config) {
console.error('test');
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the request access button");
}
// We can only requets more access if we're in read-only mode
if (config.readOnly !== 1) { return; }
var $requestBlock = $('<button>', {
'class': 'fa fa-lock cp-toolbar-share-button',
title: Messages.requestEdit_button
}).hide();
// If we have access to the owner's mailbox, display the button and enable it
// false => check if we can contact the owner
// true ==> send the request
Common.getSframeChannel().query('Q_REQUEST_ACCESS', false, function (err, obj) {
if (obj && obj.state) {
var locked = false;
$requestBlock.show().click(function () {
if (locked) { return; }
locked = true;
Common.getSframeChannel().query('Q_REQUEST_ACCESS', true, function (err, obj) {
if (obj && obj.state) {
UI.log(Messages.requestEdit_sent);
$requestBlock.hide();
} else {
locked = false;
}
});
});
}
});
toolbar.$leftside.append($requestBlock);
toolbar.request = $requestBlock;
return $requestBlock;
};
var createTitle = function (toolbar, config) { var createTitle = function (toolbar, config) {
var $titleContainer = $('<span>', { var $titleContainer = $('<span>', {
'class': TITLE_CLS 'class': TITLE_CLS
@ -724,7 +772,7 @@ MessengerUI, Messages) {
}; };
var createPageTitle = function (toolbar, config) { var createPageTitle = function (toolbar, config) {
if (config.title || !config.pageTitle) { return; } if (!config.pageTitle) { return; }
var $titleContainer = $('<span>', { var $titleContainer = $('<span>', {
'class': TITLE_CLS 'class': TITLE_CLS
}).appendTo(toolbar.$top); }).appendTo(toolbar.$top);
@ -832,11 +880,17 @@ MessengerUI, Messages) {
return $spin; return $spin;
}; };
var createLimit = function (toolbar) { var createLimit = function (toolbar, config) {
var $limitIcon = $('<span>', {'class': 'fa fa-exclamation-triangle'}); var $limitIcon = $('<span>', {'class': 'fa fa-exclamation-triangle'});
var $limit = toolbar.$userAdmin.find('.'+LIMIT_CLS).attr({ var $limit = toolbar.$userAdmin.find('.'+LIMIT_CLS).attr({
'title': Messages.pinLimitReached 'title': Messages.pinLimitReached
}).append($limitIcon).hide(); }).append($limitIcon).hide();
var priv = config.metadataMgr.getPrivateData();
var origin = priv.origin;
var l = document.createElement("a");
l.href = origin;
var todo = function (e, overLimit) { var todo = function (e, overLimit) {
if (e) { return void console.error("Unable to get the pinned usage", e); } if (e) { return void console.error("Unable to get the pinned usage", e); }
if (overLimit) { if (overLimit) {
@ -845,7 +899,7 @@ MessengerUI, Messages) {
key = 'pinLimitReachedAlertNoAccounts'; key = 'pinLimitReachedAlertNoAccounts';
} }
$limit.show().click(function () { $limit.show().click(function () {
UI.alert(Messages._getKey(key, [encodeURIComponent(window.location.hostname)]), null, true); UI.alert(Messages._getKey(key, [encodeURIComponent(l.hostname)]), null, true);
}); });
} }
}; };
@ -944,10 +998,18 @@ MessengerUI, Messages) {
var createNotifications = function (toolbar, config) { var createNotifications = function (toolbar, config) {
var $notif = toolbar.$top.find('.'+NOTIFICATIONS_CLS).show(); var $notif = toolbar.$top.find('.'+NOTIFICATIONS_CLS).show();
var openNotifsApp = h('div.cp-notifications-gotoapp', h('p', Messages.openNotificationsApp || "Open notifications App"));
$(openNotifsApp).click(function () {
Common.openURL("/notifications/");
});
var div = h('div.cp-notifications-container', [ var div = h('div.cp-notifications-container', [
h('div.cp-notifications-empty', Messages.notifications_empty) h('div.cp-notifications-empty', Messages.notifications_empty)
]); ]);
var pads_options = [div]; var pads_options = [div];
if (Common.isLoggedIn()) {
pads_options.unshift(h("hr"));
pads_options.unshift(openNotifsApp);
}
var dropdownConfig = { var dropdownConfig = {
text: '', // Button initial text text: '', // Button initial text
options: pads_options, // Entries displayed in the menu options: pads_options, // Entries displayed in the menu
@ -983,10 +1045,10 @@ MessengerUI, Messages) {
$button.addClass('fa-bell'); $button.addClass('fa-bell');
}; };
Common.mailbox.subscribe({ Common.mailbox.subscribe(['notifications'], {
onMessage: function (data, el) { onMessage: function (data, el) {
if (el) { if (el) {
div.appendChild(el); $(div).prepend(el);
} }
refresh(); refresh();
}, },
@ -1158,6 +1220,7 @@ MessengerUI, Messages) {
tb['fileshare'] = createFileShare; tb['fileshare'] = createFileShare;
tb['title'] = createTitle; tb['title'] = createTitle;
tb['pageTitle'] = createPageTitle; tb['pageTitle'] = createPageTitle;
tb['request'] = createRequest;
tb['lag'] = $.noop; tb['lag'] = $.noop;
tb['spinner'] = createSpinner; tb['spinner'] = createSpinner;
tb['state'] = $.noop; tb['state'] = $.noop;

@ -565,7 +565,7 @@
"download_step1": "Laden...", "download_step1": "Laden...",
"download_step2": "Entschlüsselung...", "download_step2": "Entschlüsselung...",
"todo_title": "CryptTodo", "todo_title": "CryptTodo",
"todo_newTodoNamePlaceholder": "Beschreibe deine Aufgabe...", "todo_newTodoNamePlaceholder": "Beschreibe deine Aufgabe",
"todo_newTodoNameTitle": "Diese Aufgabe zu deiner ToDo-Liste hinzufügen", "todo_newTodoNameTitle": "Diese Aufgabe zu deiner ToDo-Liste hinzufügen",
"todo_markAsCompleteTitle": "Diese Aufgabe als erledigt markieren", "todo_markAsCompleteTitle": "Diese Aufgabe als erledigt markieren",
"todo_markAsIncompleteTitle": "Diese Aufgabe als nicht erledigt markieren", "todo_markAsIncompleteTitle": "Diese Aufgabe als nicht erledigt markieren",
@ -830,7 +830,7 @@
"generic": { "generic": {
"more": "Erfahre mehr über die Nutzung von CryptPad, indem du unsere <a href=\"/faq.html\" target=\"_blank\">FAQ</a> liest", "more": "Erfahre mehr über die Nutzung von CryptPad, indem du unsere <a href=\"/faq.html\" target=\"_blank\">FAQ</a> liest",
"share": "Benutze das Teilen-Menü (<span class=\"fa fa-share-alt\"></span>), um Links zu generieren, die Mitarbeiter zum Lesen oder Bearbeiten einladen", "share": "Benutze das Teilen-Menü (<span class=\"fa fa-share-alt\"></span>), um Links zu generieren, die Mitarbeiter zum Lesen oder Bearbeiten einladen",
"save": "Alle Änderungen werden automatisch synchronisiert. Du misst sie also nicht speichern" "save": "Alle Änderungen werden automatisch synchronisiert. Du musst sie also nicht selbst speichern"
}, },
"text": { "text": {
"formatting": "Du kannst die Werkzeugleiste anzeigen oder verbergen, indem du auf <span class=\"fa fa-caret-down\"></span> oder <span class=\"fa fa-caret-up\"></span> klickst", "formatting": "Du kannst die Werkzeugleiste anzeigen oder verbergen, indem du auf <span class=\"fa fa-caret-down\"></span> oder <span class=\"fa fa-caret-up\"></span> klickst",
@ -1054,7 +1054,7 @@
"friendRequest_accepted": "<b>{0}</b> hat deine Freundschaftsanfrage akzeptiert", "friendRequest_accepted": "<b>{0}</b> hat deine Freundschaftsanfrage akzeptiert",
"friendRequest_received": "<b>{0}</b> möchte mit dir befreundet sein", "friendRequest_received": "<b>{0}</b> möchte mit dir befreundet sein",
"friendRequest_notification": "<b>{0}</b> hat dir eine Freundschaftsanfrage geschickt", "friendRequest_notification": "<b>{0}</b> hat dir eine Freundschaftsanfrage geschickt",
"notifications_empty": "Du hast keine neuen Benachrichtigungen", "notifications_empty": "Keine Benachrichtigungen verfügbar",
"notifications_title": "Du hast ungelesene Benachrichtigungen", "notifications_title": "Du hast ungelesene Benachrichtigungen",
"profile_addDescription": "Beschreibung hinzufügen", "profile_addDescription": "Beschreibung hinzufügen",
"profile_editDescription": "Deine Beschreibung bearbeiten", "profile_editDescription": "Deine Beschreibung bearbeiten",
@ -1074,5 +1074,54 @@
"fm_info_sharedFolderHistory": "Dies ist nur der Verlauf deines geteilten Ordners: <b>{0}</b><br/>Dein CryptDrive bleibt beim Navigieren im Nur-Lesen-Modus.", "fm_info_sharedFolderHistory": "Dies ist nur der Verlauf deines geteilten Ordners: <b>{0}</b><br/>Dein CryptDrive bleibt beim Navigieren im Nur-Lesen-Modus.",
"share_description": "Wähle aus, was du teilen möchtest. Dir wird dann ein entsprechender Link anzeigt. Du kannst es auch direkt an deine Freunde in CryptPad senden.", "share_description": "Wähle aus, was du teilen möchtest. Dir wird dann ein entsprechender Link anzeigt. Du kannst es auch direkt an deine Freunde in CryptPad senden.",
"fc_expandAll": "Alle ausklappen", "fc_expandAll": "Alle ausklappen",
"fc_collapseAll": "Alle einklappen" "fc_collapseAll": "Alle einklappen",
"fc_color": "Farbe ändern",
"supportPage": "Support",
"admin_cat_support": "Support",
"admin_supportInitHelp": "Dein Server ist noch nicht für die Verwendung eines Support-Postfaches konfiguriert. Wenn du ein Support-Postfach verwenden möchtest, um Nachrichten von Benutzern zu empfangen, bitte deinen Server-Administrator das Skript in \"./scripts/generate-admin-keys.js\" auszuführen, den öffentlichen Schlüssel in der Datei \"config.js\" zu speichern und dir den privaten Schlüssel zuzusenden.",
"admin_supportInitPrivate": "Deine CryptPad-Instanz ist für die Verwendung eines Support-Postfaches konfiguriert. Allerdings verfügt dein Account nicht über den für den Zugriff benötigten privaten Schlüssel. Bitte benutze folgendes Formular, um den privaten Schlüssel zu deinem Account hinzuzufügen oder zu aktualisieren.",
"admin_supportAddKey": "Privaten Schlüssel hinzufügen",
"admin_supportAddError": "Privater Schlüssel ist ungültig",
"admin_supportInitTitle": "Einrichtung des Support-Postfaches",
"admin_supportInitHint": "Du kannst ein Support-Postfach einrichten, damit dich deine Nutzer bei Problemen auf einem sicheren Weg kontaktieren können.",
"admin_supportListTitle": "Support-Postfach",
"admin_supportListHint": "Hier ist die Liste der an das Support-Postfach gesendeten Tickets. Alle Administratoren können die Nachrichten und Antworten sehen. Ein geschlossenes Ticket kann nicht wieder geöffnet werden. Du kannst geschlossene Tickets nur ausblenden, sie bleiben aber für die andere Administratoren sichtbar.",
"support_disabledTitle": "Support ist nicht aktiviert",
"support_disabledHint": "Diese CryptPad-Instanz wurde noch nicht für die Verwendung eines Support-Formulars konfiguriert.",
"support_cat_new": "Neues Ticket",
"support_formTitle": "Titel des Tickets",
"support_formHint": "Mit diesem Formular kann ein neues Support-Ticket eröffnet werden. Es erlaubt die sichere Kontaktaufnahme mit den Administratoren zur Lösung von Problemen oder Beantwortung von Fragen. Bitte eröffne kein neues Ticket, wenn du bereits ein offenes Ticket bezüglich des gleichen Problems hast. Verwende stattdessen die Antworten-Schaltfläche, um weitere Informationen hinzuzufügen.",
"support_formButton": "Absenden",
"support_formTitleError": "Fehler: Titel ist leer",
"support_formContentError": "Fehler: Inhalt ist leer",
"support_formMessage": "Gib deine Nachricht ein...",
"support_cat_tickets": "Vorhandene Tickets",
"support_listTitle": "Support-Tickets",
"support_listHint": "Hier ist die Liste der an die Administratoren gesendeten Tickets und der dazugehörigen Antworten. Ein geschlossenes Ticket kann nicht wieder geöffnet werden, du musst ein Ticket eröffnen. Du kannst geschlossene Tickets ausblenden, aber sie werden weiterhin für die Administratoren sichtbar sein.",
"support_answer": "Antworten",
"support_close": "Ticket schließen",
"support_remove": "Ticket entfernen",
"support_showData": "Benutzerdaten anzeigen/verbergen",
"support_from": "<b>Von:</b> {0}",
"support_closed": "Dieses Ticket wurde geschlossen",
"fc_noAction": "Keine Aktion verfügbar",
"notificationsPage": "Benachrichtigungen",
"openNotificationsApp": "Benachrichtigungspanel öffnen",
"notifications_cat_all": "Alle",
"notifications_cat_friends": "Freundschaftsanfragen",
"notifications_cat_pads": "Mit mir geteilt",
"notifications_cat_archived": "Verlauf",
"notifications_dismissAll": "Alle verbergen",
"support_notification": "",
"requestEdit_button": "",
"requestEdit_dialog": "",
"requestEdit_confirm": "",
"requestEdit_fromFriend": "",
"requestEdit_fromStranger": "",
"requestEdit_viewPad": "",
"later": "",
"requestEdit_request": "",
"requestEdit_accepted": "",
"requestEdit_sent": "",
"uploadFolderButton": ""
} }

@ -1056,7 +1056,7 @@
"friendRequest_accepted": "<b>{0}</b> a accepté votre demande d'ami", "friendRequest_accepted": "<b>{0}</b> a accepté votre demande d'ami",
"friendRequest_received": "<b>{0}</b> souhaite être votre ami", "friendRequest_received": "<b>{0}</b> souhaite être votre ami",
"friendRequest_notification": "<b>{0}</b> vous a envoyé une demande d'ami", "friendRequest_notification": "<b>{0}</b> vous a envoyé une demande d'ami",
"notifications_empty": "Vous n'avez pas de nouvelle notification", "notifications_empty": "Pas de nouvelle notification",
"notifications_title": "Vous avez des notifications non lues", "notifications_title": "Vous avez des notifications non lues",
"profile_addDescription": "Ajouter une description", "profile_addDescription": "Ajouter une description",
"profile_editDescription": "Modifier votre description", "profile_editDescription": "Modifier votre description",
@ -1074,5 +1074,42 @@
"share_withFriends": "Partager", "share_withFriends": "Partager",
"notifications_dismiss": "Cacher", "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.", "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." "share_description": "Choisissez ce que vous souhaitez partager puis obtenez le lien ou envoyez-le directement à vos amis CryptPad.",
"fc_color": "Changer la couleur",
"supportPage": "Support",
"admin_cat_support": "Support",
"admin_supportAddKey": "Ajouter la clé",
"admin_supportAddError": "Clé privée non valide",
"admin_supportInitTitle": "Initialisation du support",
"admin_supportListTitle": "Messagerie du support",
"support_disabledTitle": "Le support n'est pas activé",
"support_disabledHint": "Cette instance de CryptPad n'est pas encore configurée pour utiliser le formulaire de support.",
"support_cat_new": "Nouveau ticket",
"support_formTitle": "Titre du ticket",
"support_formButton": "Envoyer",
"support_formTitleError": "Erreur : le titre est vide",
"support_formContentError": "Erreur : le contenu est vide",
"support_formMessage": "Taper votre message...",
"support_cat_tickets": "Tickets existants",
"support_listTitle": "Tickets de support",
"support_answer": "Répondre",
"support_close": "Fermer le ticket",
"support_remove": "Supprimer le ticket",
"support_showData": "Afficher/cacher les données de l'utilisateur",
"support_from": "<b>De :</b> {0}",
"support_closed": "Ce ticket a été fermé",
"fc_noAction": "Pas d'action disponible",
"notificationsPage": "Notifications",
"openNotificationsApp": "Ouvrir le panneau de notifications",
"notifications_cat_all": "Toutes",
"notifications_cat_friends": "Demandes d'ami",
"notifications_cat_pads": "Partagé avec moi",
"notifications_cat_archived": "Historique",
"notifications_dismissAll": "Tout cacher",
"admin_supportInitHelp": "Votre serveur n'est pas configuré pour avoir une messagerie de support. Si vous souhaitez activer cette messagerie pour recevoir des messages des utilisateurs, vous devez demander à l'administrateur du serveur d'exécuter le script situé dans \"./scripts/generate-admin-keys.js\", de stocker la clé publique générée dans \"config.js\" puis de vous envoyer la clé privée.",
"admin_supportInitPrivate": "Votre instance de CryptPad est configurée pour utiliser la messagerie de support mais votre compte utilisateur ne connaît pas la bonne clé privée nécessaire pour y avoir accès. Veuillez utiliser le formulaire suivant pour ajouter ou mettre à jour la clé dans votre compte.",
"admin_supportInitHint": "Vous pouvez configurer une messagerie de support afin de fournir aux utilisateurs de votre instance CryptPad un moyen de vous contacter de manière sécurisée en cas de problème avec leur compte.",
"admin_supportListHint": "Voici la liste des tickets envoyés par les utilisateurs au support. Tous les administrateurs peuvent voir les tickets et leurs réponses. Un ticket fermé ne peut pas être ré-ouvert. Vous ne pouvez supprimer (ou cacher) que les tickets fermés, et les tickets supprimés restent visible par les autres administrateurs.",
"support_formHint": "Ce formulaire peut être utilisé pour créer un nouveau ticket de support. Utilisez-le pour contacter les administrateurs de manière sécurisée afin de résoudre un problème ou d'obtenir des renseignements. Merci de ne pas créer de nouveau ticket si vous avez déjà un ticket ouvert concernant le même problème, vous pouvez utiliser le bouton \"Répondre\" dans ce cas.",
"support_listHint": "Voici la liste des tickets envoyés au support, ainsi que les réponses. Un ticket fermé ne peut pas être ré-ouvert, mais il est possible d'en créer un nouveau. Vous pouvez cacher les tickets qui ont été fermés."
} }

@ -1057,7 +1057,7 @@
"friendRequest_accepted": "<b>{0}</b> accepted your friend request", "friendRequest_accepted": "<b>{0}</b> accepted your friend request",
"friendRequest_received": "<b>{0}</b> would like to be your friend", "friendRequest_received": "<b>{0}</b> would like to be your friend",
"friendRequest_notification": "<b>{0}</b> sent you a friend request", "friendRequest_notification": "<b>{0}</b> sent you a friend request",
"notifications_empty": "You have no new notifications", "notifications_empty": "No notifications available",
"notifications_title": "You have unread notifications", "notifications_title": "You have unread notifications",
"profile_addDescription": "Add a description", "profile_addDescription": "Add a description",
"profile_editDescription": "Edit your description", "profile_editDescription": "Edit your description",
@ -1075,5 +1075,54 @@
"share_withFriends": "Share", "share_withFriends": "Share",
"notifications_dismiss": "Dismiss", "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.", "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." "share_description": "Choose what you'd like to share and either get the link or send it directly to your CryptPad friends.",
"supportPage": "Support",
"admin_cat_support": "Support",
"admin_supportInitHelp": "Your server is not yet configured to have a support mailbox. If you want a support mailbox to receive messages from your users, you should ask your server administrator to run the script located in \"./scripts/generate-admin-keys.js\", then store the public key in the \"config.js\" file and send you the private key.",
"admin_supportInitPrivate": "Your CryptPad instance is configured to use a support mailbox but your account doesn't have the correct private key to access it. Please use the following form to add or update the private key to your account.",
"admin_supportAddKey": "Add private key",
"admin_supportAddError": "Invalid private key",
"admin_supportInitTitle": "Support mailbox initialization",
"admin_supportInitHint": "You can configure a support mailbox in order to give users of your CryptPad instance a way to contact you securely if they have an issue with their account.",
"admin_supportListTitle": "Support mailbox",
"admin_supportListHint": "Here is the list of tickets sent by users to the support mailbox. All the administrators can see the messages and their answers. A closed ticket cannot be re-opened. You can only remove (hide) closed tickets, and the removed tickets are still visible by other administrators.",
"support_disabledTitle": "Support is not enabled",
"support_disabledHint": "This CryptPad instance is not yet configured to use a support form.",
"support_cat_new": "New ticket",
"support_formTitle": "Ticket title",
"support_formHint": "This form can be used to create a new support ticket. Use it to contact the administrators to solve issues or ask any question in a secure way. Please don't create a new ticket if you already have an open ticket about the same issue, but use the reply button to provide more information.",
"support_formButton": "Send",
"support_formTitleError": "Error: title is empty",
"support_formContentError": "Error: content is empty",
"support_formMessage": "Type your message...",
"support_cat_tickets": "Existing tickets",
"support_listTitle": "Support tickets",
"support_listHint": "Here is the list of tickets sent to the administrators and their answers. A closed ticket cannot be re-opened but you can make a new one. You can hide tickets that have been closed.",
"support_answer": "Reply",
"support_close": "Close the ticket",
"support_remove": "Remove the ticket",
"support_showData": "Show/hide user data",
"support_from": "<b>From:</b> {0}",
"support_closed": "This ticket has been closed",
"fc_noAction": "No action available",
"notificationsPage": "Notifications",
"openNotificationsApp": "Open notifications panel",
"notifications_cat_all": "All",
"notifications_cat_friends": "Friend requests",
"notifications_cat_pads": "Shared with me",
"notifications_cat_archived": "History",
"notifications_dismissAll": "Dismiss all",
"support_notification": "An administrator has responded to your support ticket",
"requestEdit_button": "Request edit rights",
"requestEdit_dialog": "Are you sure you'd like to ask the owner of this pad for the ability to edit?",
"requestEdit_confirm": "{1} has asked for the ability to edit the pad <b>{0}</b>. Would you like to grant them access?",
"requestEdit_fromFriend": "You are friends with {0}",
"requestEdit_fromStranger": "You are <b>not</b> friends with {0}",
"requestEdit_viewPad": "Open the pad in a new tab",
"later": "Decide later",
"requestEdit_request": "{1} wants to edit the pad <b>{0}</b>",
"requestEdit_accepted": "{1} granted you edit rights for the pad <b>{0}</b>",
"requestEdit_sent": "Request sent",
"uploadFolderButton": "Upload folder",
"properties_unknownUser": "{0} unknown user(s)"
} }

@ -389,5 +389,64 @@
"fc_empty": "Удалить корзину", "fc_empty": "Удалить корзину",
"fc_prop": "Свойства", "fc_prop": "Свойства",
"fc_hashtag": "Теги", "fc_hashtag": "Теги",
"fc_sizeInKilobytes": "Размер в килобайтах" "fc_sizeInKilobytes": "Размер в килобайтах",
"poll_title": "Приватный выбор даты",
"fm_moveNestedSF": "Нельзя помещать одну общую папку в другую. Папка {0} не была перемещена.",
"fc_color": "Изменить цвет",
"fc_expandAll": "Расширить все",
"fc_collapseAll": "Скрыть все",
"fc_remove": "Удалить из вашего CryptDrive",
"fo_moveUnsortedError": "Вы не можете переместить папку в список черновиков",
"fo_existingNameError": "Это имя уже используется в данной директории. Пожалуйста выберите другое.",
"fo_unableToRestore": "Невозможно восстановить этот файл в исходное местоположение. Вы можете попытаться переместить его в другое место.",
"login_login": "Войти",
"login_makeAPad": "Создать анонимный пэд",
"login_nologin": "Просмотреть локальные пэды",
"login_register": "Зарегистрироваться",
"logoutButton": "Выйти",
"settingsButton": "Настройки",
"login_username": "Имя пользователя",
"login_password": "Пароль",
"login_confirm": "Подтвердите ваш пароль",
"login_remember": "Запомнить меня",
"login_hashing": "Ваш пароль хэшируется, это может занять некое время.",
"login_hello": "Привет {0},",
"login_helloNoName": "Привет,",
"fm_info_sharedFolder": "Это общая папка. Вы не вошли в систему, поэтому можете получить к ней доступ только в режиме только для чтения.<br><a href=\"/register/\">Sign up</a> или <a href=\"/login/\">Log in</a> для импорта на CryptDrive и его изменения.",
"fo_moveFolderToChildError": "Вы не можете переместить папку в одну из нее следующую",
"fo_unavailableName": "Файл или папка с таким же именем уже существуют в новом месте. Переименуйте элемент и повторите попытку.",
"fs_migration": "Ваш CryptDrive обновляется до новой версии. В результате, текущая страница должна быть перезагружена.<br><strong> перезагрузите эту страницу, чтобы продолжить ей пользоваться.</strong>.",
"login_accessDrive": "Доступ к хранилищу",
"login_orNoLogin": "или",
"login_noSuchUser": "Неверный логин или пароль. Попробуйте еще раз или зарегистрируйтесь",
"login_invalUser": "Неоьходимо имя пользователя",
"login_invalPass": "Необходим пароль",
"login_unhandledError": "Произошла неожиданная ошибка :(",
"register_importRecent": "Импортировать пэды из вашей анонимной сессии",
"register_passwordsDontMatch": "Пароли не совпадают!",
"register_passwordTooShort": "Длина пароля должна составлять не менее {0} символов.",
"register_mustAcceptTerms": "Вы должны принять условия пользования.",
"register_mustRememberPass": "Мы не сможем сбросить ваш пароль, если вы его забудете. Очень важно, чтобы вы его запомнили! Пожалуйста, отметьте флажок для подтверждения.",
"register_whyRegister": "Почему стоит зарегистрироваться?",
"register_header": "Добро пожаловать в CryptPad",
"register_writtenPassword": "Я записал свое имя пользователя и пароль, продолжить",
"register_cancel": "Назад",
"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_backupHint": "Резервное копирование или восстановление всего содержимого CryptDrive. Он не будет содержать содержимое ваших пэдов, только ключи для доступа к ним.",
"settings_restore": "Восстановить",
"settings_exportDescription": "Пожалуйста, подождите, пока мы загружаем и расшифровываем ваши документы. Это может занять несколько минут. Закрытие вкладки прервет процесс.",
"settings_exportFailed": "Если загрузка пэда занимает более 1 минуты, он не будет включен в экспорт. Отображается ссылка на любой блокнот, который не был экспортирован.",
"settings_exportWarning": "Примечание: этот инструмент все еще находится в бета-версии и может иметь проблемы со масштабируемостью. Для повышения производительности рекомендуется оставить данную вкладку сфокусированной.",
"settings_exportCancel": "Вы уверены, что хотите отменить экспорт? В следующий раз вам придется начинать все сначала.",
"settings_export_reading": "Читаем ваше хранилище...",
"settings_export_download": "Скачиваем и расшифровываем ваши документы..."
} }

@ -311,12 +311,12 @@ define([
_getFiles[FILES_DATA] = function () { _getFiles[FILES_DATA] = function () {
var ret = []; var ret = [];
if (!files[FILES_DATA]) { return ret; } if (!files[FILES_DATA]) { return ret; }
return Object.keys(files[FILES_DATA]).map(Number); return Object.keys(files[FILES_DATA]).map(Number).filter(Boolean);
}; };
_getFiles[SHARED_FOLDERS] = function () { _getFiles[SHARED_FOLDERS] = function () {
var ret = []; var ret = [];
if (!files[SHARED_FOLDERS]) { return ret; } if (!files[SHARED_FOLDERS]) { return ret; }
return Object.keys(files[SHARED_FOLDERS]).map(Number); return Object.keys(files[SHARED_FOLDERS]).map(Number).filter(Boolean);
}; };
var getFiles = exp.getFiles = function (categories) { var getFiles = exp.getFiles = function (categories) {
var ret = []; var ret = [];
@ -519,26 +519,17 @@ define([
var resFolders = []; var resFolders = [];
var findFoldersRec = function (folder, path) { var findFoldersRec = function (folder, path) {
for (var key in folder) { for (var key in folder) {
if (isFolder(folder[key])) { if (isFolder(folder[key]) && !isSharedFolder(folder[key])) {
if (isSharedFolder(folder[key])) { if (key.toLowerCase().indexOf(lValue) !== -1) {
// var name = getSharedFolderData(folder[key]).title || ""; resFolders.push({
// if (name.toLowerCase().indexOf(lValue) !== -1) { id: null,
// resFolders.push(path.concat([key, ROOT])); paths: [path.concat(key)],
// } data: {
findFoldersRec(folder[key], path.concat([key, ROOT])); title: key
} }
else { });
if (key.toLowerCase().indexOf(lValue) !== -1) {
resFolders.push({
id: null,
paths: [path.concat(key)],
data: {
title: key
}
});
}
findFoldersRec(folder[key], path.concat(key));
} }
findFoldersRec(folder[key], path.concat(key));
} }
} }
}; };

@ -30,6 +30,7 @@
@drive_content-bg-ro: darken(@drive_content-bg, 10%); @drive_content-bg-ro: darken(@drive_content-bg, 10%);
@drive_selected-bg: #888; @drive_selected-bg: #888;
@drive_droppable-bg: #FE9A2E;
/* PAGE */ /* PAGE */
@ -107,7 +108,7 @@
.cp-app-drive-container { .cp-app-drive-container {
flex: 1; flex: 1;
overflow: auto; overflow-x: auto;
width: 100%; width: 100%;
display: flex; display: flex;
flex-flow: row; flex-flow: row;
@ -121,6 +122,7 @@
#cp-app-drive-tree { #cp-app-drive-tree {
resize: none; resize: none;
width: 100% !important; width: 100% !important;
min-width: unset;
max-width: unset; max-width: unset;
max-height: unset; max-height: unset;
border-bottom: 1px solid @drive_mobile-tree-border-col; border-bottom: 1px solid @drive_mobile-tree-border-col;
@ -156,7 +158,7 @@
} }
.cp-app-drive-element-droppable { .cp-app-drive-element-droppable {
background-color: #FE9A2E; background-color: @drive_droppable-bg;
color: #222; color: #222;
} }
@ -239,7 +241,6 @@
max-height: 100%; max-height: 100%;
.cp-app-drive-tree-categories-container { .cp-app-drive-tree-categories-container {
flex: 1; flex: 1;
max-width: 500px;
overflow: auto; overflow: auto;
} }
img.cp-app-drive-icon { img.cp-app-drive-icon {
@ -438,13 +439,13 @@
flex: 1; flex: 1;
// Needed to avoid the folder's path to overflows // Needed to avoid the folder's path to overflows
// https://stackoverflow.com/questions/38223879/white-space-nowrap-breaks-flexbox-layout // https://stackoverflow.com/questions/38223879/white-space-nowrap-breaks-flexbox-layout
min-width: 0; // min-width: 0;
} }
#cp-app-drive-content { #cp-app-drive-content {
box-sizing: border-box; box-sizing: border-box;
background: @drive_content-bg; background: @drive_content-bg;
color: @drive_content-fg; color: @drive_content-fg;
overflow: auto; overflow-y: auto;
flex: 1; flex: 1;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
@ -939,6 +940,7 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
transition: all 0.15s; transition: all 0.15s;
cursor: pointer;
&:first-child { &:first-child {
flex-shrink: 1; flex-shrink: 1;
@ -946,17 +948,20 @@
&.cp-app-drive-path-separator { &.cp-app-drive-path-separator {
color: #ccc; color: #ccc;
cursor: default;
} }
&.cp-app-drive-path-collapse { &.cp-app-drive-path-collapse {
position: relative; position: relative;
} }
&:hover { &.cp-app-drive-element-droppable {
background-color: @drive_droppable-bg;
}
&:not(.cp-app-drive-element-droppable):hover {
&:not(.cp-app-drive-path-separator) { &:not(.cp-app-drive-path-separator) {
background-color: darken(@colortheme_drive-bg, 15%); background-color: darken(@colortheme_drive-bg, 15%);
text-decoration: underline; text-decoration: underline;
cursor: pointer;
} }
& ~ .cp-app-drive-path-element { & ~ .cp-app-drive-path-element {
background-color: darken(@colortheme_drive-bg, 15%); background-color: darken(@colortheme_drive-bg, 15%);

@ -17,8 +17,6 @@ define([
'/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-listmap/chainpad-listmap.js',
'/customize/messages.js', '/customize/messages.js',
'/common/jscolor.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/drive/app-drive.less', 'less!/drive/app-drive.less',
@ -43,7 +41,10 @@ define([
{ {
var APP = window.APP = { var APP = window.APP = {
editable: false, editable: false,
mobile: function () { return $('body').width() <= 600; }, // Menu and content area are not inline-block anymore for mobiles mobile: function () {
if (window.matchMedia) { return !window.matchMedia('(any-pointer:fine)').matches; }
else { return $('body').width() <= 600; }
},
isMac: navigator.platform === "MacIntel", isMac: navigator.platform === "MacIntel",
}; };
@ -77,6 +78,8 @@ define([
var faFolderOpen = 'cptools-folder-open'; var faFolderOpen = 'cptools-folder-open';
var faSharedFolder = 'cptools-shared-folder'; var faSharedFolder = 'cptools-shared-folder';
var faSharedFolderOpen = 'cptools-shared-folder-open'; var faSharedFolderOpen = 'cptools-shared-folder-open';
var faExpandAll = 'fa-plus-square-o';
var faCollapseAll = 'fa-minus-square-o';
var faShared = 'fa-shhare-alt'; var faShared = 'fa-shhare-alt';
var faReadOnly = 'fa-eye'; var faReadOnly = 'fa-eye';
var faRename = 'fa-pencil'; var faRename = 'fa-pencil';
@ -297,6 +300,33 @@ define([
}); });
}; };
APP.selectedFiles = [];
var isElementSelected = function ($element) {
var elementId = $element.data("path").slice(-1)[0];
return APP.selectedFiles.indexOf(elementId) !== -1;
};
var selectElement = function ($element) {
var elementId = $element.data("path").slice(-1)[0];
if (APP.selectedFiles.indexOf(elementId) === -1) {
APP.selectedFiles.push(elementId);
}
$element.addClass("cp-app-drive-element-selected");
};
var unselectElement = function ($element) {
var elementId = $element.data("path").slice(-1)[0];
var index = APP.selectedFiles.indexOf(elementId);
if (index !== -1) {
APP.selectedFiles.splice(index, 1);
}
$element.removeClass("cp-app-drive-element-selected");
};
var findSelectedElements = function () {
return $(".cp-app-drive-element-selected");
};
var createContextMenu = function () { var createContextMenu = function () {
var menu = h('div.cp-contextmenu.dropdown.cp-unselectable', [ var menu = h('div.cp-contextmenu.dropdown.cp-unselectable', [
h('ul.dropdown-menu', { h('ul.dropdown-menu', {
@ -316,11 +346,11 @@ define([
$separator.clone()[0], $separator.clone()[0],
h('li', h('a.cp-app-drive-context-expandall.dropdown-item', { h('li', h('a.cp-app-drive-context-expandall.dropdown-item', {
'tabindex': '-1', 'tabindex': '-1',
'data-icon': "expandAll", 'data-icon': faExpandAll,
}, Messages.fc_expandAll)), }, Messages.fc_expandAll)),
h('li', h('a.cp-app-drive-context-collapseall.dropdown-item', { h('li', h('a.cp-app-drive-context-collapseall.dropdown-item', {
'tabindex': '-1', 'tabindex': '-1',
'data-icon': "collapseAll", 'data-icon': faCollapseAll,
}, Messages.fc_collapseAll)), }, Messages.fc_collapseAll)),
$separator.clone()[0], $separator.clone()[0],
h('li', h('a.cp-app-drive-context-color.dropdown-item.cp-app-drive-context-editable', { h('li', h('a.cp-app-drive-context-color.dropdown-item.cp-app-drive-context-editable', {
@ -543,13 +573,14 @@ define([
// Tags used: display Tags category // Tags used: display Tags category
if (Object.keys(manager.getTagsList()).length) { displayedCategories.push(TAGS); } if (Object.keys(manager.getTagsList()).length) { displayedCategories.push(TAGS); }
var virtualCategories = [SEARCH, RECENT, OWNED, TAGS, SHARED_FOLDER]; var virtualCategories = [SEARCH, RECENT, OWNED, TAGS];
if (!APP.loggedIn) { if (!APP.loggedIn) {
$tree.hide(); $tree.hide();
if (APP.newSharedFolder) { if (APP.newSharedFolder) {
// ANON_SHARED_FOLDER // ANON_SHARED_FOLDER
displayedCategories = [SHARED_FOLDER]; displayedCategories = [SHARED_FOLDER];
virtualCategories.push(SHARED_FOLDER);
currentPath = [SHARED_FOLDER, ROOT]; currentPath = [SHARED_FOLDER, ROOT];
} else { } else {
displayedCategories = [FILES_DATA]; displayedCategories = [FILES_DATA];
@ -595,7 +626,8 @@ define([
var sel = {}; var sel = {};
var removeSelected = function (keepObj) { var removeSelected = function (keepObj) {
$('.cp-app-drive-element-selected').removeClass("cp-app-drive-element-selected"); APP.selectedFiles = [];
findSelectedElements().removeClass("cp-app-drive-element-selected");
var $container = $driveToolbar.find('#cp-app-drive-toolbar-contextbuttons'); var $container = $driveToolbar.find('#cp-app-drive-toolbar-contextbuttons');
if (!$container.length) { return; } if (!$container.length) { return; }
$container.html(''); $container.html('');
@ -705,7 +737,9 @@ define([
delete sel.move; delete sel.move;
$content.find('.cp-app-drive-element-selected-tmp') $content.find('.cp-app-drive-element-selected-tmp')
.removeClass('cp-app-drive-element-selected-tmp') .removeClass('cp-app-drive-element-selected-tmp')
.addClass('cp-app-drive-element-selected'); .each(function (idx, element) {
selectElement($(element));
});
e.stopPropagation(); e.stopPropagation();
}); });
@ -740,7 +774,9 @@ define([
// Ctrl+A select all // Ctrl+A select all
if (e.which === 65 && (e.ctrlKey || (e.metaKey && APP.isMac))) { if (e.which === 65 && (e.ctrlKey || (e.metaKey && APP.isMac))) {
$content.find('.cp-app-drive-element:not(.cp-app-drive-element-selected)') $content.find('.cp-app-drive-element:not(.cp-app-drive-element-selected)')
.addClass('cp-app-drive-element-selected'); .each(function (idx, element) {
selectElement($(element));
});
return; return;
} }
@ -753,7 +789,7 @@ define([
APP.onElementClick(ev, $(el)); APP.onElementClick(ev, $(el));
}; };
var $selection = $content.find('.cp-app-drive-element.cp-app-drive-element-selected'); var $selection = findSelectedElements();
if ($selection.length === 0) { return void click($elements.first()[0]); } if ($selection.length === 0) { return void click($elements.first()[0]); }
var lastIndex = typeof sel.endSelected === "number" ? sel.endSelected : var lastIndex = typeof sel.endSelected === "number" ? sel.endSelected :
@ -872,12 +908,12 @@ define([
return; return;
} }
removeInput(); removeInput();
removeSelected();
var $name = $element.find('.cp-app-drive-element-name'); var $name = $element.find('.cp-app-drive-element-name');
if (!$name.length) { if (!$name.length) {
$name = $element.find('> .cp-app-drive-element'); $name = $element.find('> .cp-app-drive-element');
} }
$name.hide(); $name.hide();
var isFolder = $element.is(".cp-app-drive-element-folder:not(.cp-app-drive-element-sharedf)");
var el = manager.find(path); var el = manager.find(path);
var name = manager.isFile(el) ? manager.getTitle(el) : path[path.length - 1]; var name = manager.isFile(el) ? manager.getTitle(el) : path[path.length - 1];
if (manager.isSharedFolder(el)) { if (manager.isSharedFolder(el)) {
@ -899,14 +935,21 @@ define([
var newName = $input.val(); var newName = $input.val();
if (JSON.stringify(path) === JSON.stringify(currentPath)) { if (JSON.stringify(path) === JSON.stringify(currentPath)) {
manager.rename(path, $input.val(), function () { manager.rename(path, $input.val(), function () {
renameFoldersOpened(path, newName); if (isFolder) {
path[path.length - 1] = newName; renameFoldersOpened(path, newName);
path[path.length - 1] = newName;
}
APP.displayDirectory(path); APP.displayDirectory(path);
}); });
} }
else { else {
manager.rename(path, $input.val(), function () { manager.rename(path, $input.val(), function () {
renameFoldersOpened(path, newName); if (isFolder) {
renameFoldersOpened(path, newName);
unselectElement($element);
$element.data("path", $element.data("path").slice(0, -1).concat(newName));
selectElement($element);
}
refresh(); refresh();
}); });
} }
@ -927,7 +970,6 @@ define([
// We don't want to open the file/folder when clicking on the input // We don't want to open the file/folder when clicking on the input
$input.on('click dblclick', function (e) { $input.on('click dblclick', function (e) {
removeSelected();
e.stopPropagation(); e.stopPropagation();
}); });
// Remove the browser ability to drag text from the input to avoid // Remove the browser ability to drag text from the input to avoid
@ -1147,8 +1189,9 @@ define([
var getSelectedPaths = function ($element) { var getSelectedPaths = function ($element) {
var paths = []; var paths = [];
if ($('.cp-app-drive-element-selected').length > 1) { if (!$element || $element.length === 0) { return paths; }
var $selected = $('.cp-app-drive-element-selected'); if (findSelectedElements().length > 1) {
var $selected = findSelectedElements();
$selected.each(function (idx, elmt) { $selected.each(function (idx, elmt) {
var ePath = $(elmt).data('path'); var ePath = $(elmt).data('path');
if (ePath) { if (ePath) {
@ -1177,7 +1220,7 @@ define([
} else { } else {
$driveToolbar.find('cp-app-drive-toolbar-emptytrash').hide(); $driveToolbar.find('cp-app-drive-toolbar-emptytrash').hide();
} }
var $li = $content.find('.cp-app-drive-element-selected'); var $li = findSelectedElements();
if ($li.length === 0) { if ($li.length === 0) {
$li = findDataHolder($tree.find('.cp-app-drive-element-active')); $li = findDataHolder($tree.find('.cp-app-drive-element-active'));
} }
@ -1244,6 +1287,7 @@ define([
if (pos+eh <= h && pos >= 0) { return; } if (pos+eh <= h && pos >= 0) { return; }
$content.scrollTop(v); $content.scrollTop(v);
}; };
// Add the "selected" class to the "li" corresponding to the clicked element // Add the "selected" class to the "li" corresponding to the clicked element
var onElementClick = APP.onElementClick = function (e, $element) { var onElementClick = APP.onElementClick = function (e, $element) {
// If "Ctrl" is pressed, do not remove the current selection // If "Ctrl" is pressed, do not remove the current selection
@ -1280,23 +1324,23 @@ define([
var $el; var $el;
removeSelected(true); removeSelected(true);
sel.oldSelection.forEach(function (el) { sel.oldSelection.forEach(function (el) {
if (!$(el).hasClass("cp-app-drive-element-selected")) { if (!isElementSelected($(el))) {
$(el).addClass("cp-app-drive-element-selected"); selectElement($(el));
} }
}); });
for (var i = Math.min(sel.startSelected, sel.endSelected); for (var i = Math.min(sel.startSelected, sel.endSelected);
i <= Math.max(sel.startSelected, sel.endSelected); i <= Math.max(sel.startSelected, sel.endSelected);
i++) { i++) {
$el = $($elements.get(i)); $el = $($elements.get(i));
if (!$el.hasClass("cp-app-drive-element-selected")) { if (!isElementSelected($el)) {
$el.addClass("cp-app-drive-element-selected"); selectElement($el);
} }
} }
} else { } else {
if (!$element.hasClass("cp-app-drive-element-selected")) { if (!isElementSelected($element)) {
$element.addClass("cp-app-drive-element-selected"); selectElement($element);
} else { } else {
$element.removeClass("cp-app-drive-element-selected"); unselectElement($element);
} }
} }
updateContextButton(); updateContextButton();
@ -1340,7 +1384,14 @@ define([
}); });
// show contextmenu at cursor position // show contextmenu at cursor position
$menu.css({ display: "block" }); $menu.css({ display: "block" });
if (APP.mobile()) { return; } if (APP.mobile()) {
$menu.css({
top: ($("#cp-app-drive-toolbar-context-mobile").offset().top + 32) + 'px',
right: '0px',
left: ''
});
return;
}
var h = $menu.outerHeight(); var h = $menu.outerHeight();
var w = $menu.outerWidth(); var w = $menu.outerWidth();
var wH = window.innerHeight; var wH = window.innerHeight;
@ -1393,6 +1444,17 @@ define([
} else { } else {
var $element = findDataHolder($(e.target)); var $element = findDataHolder($(e.target));
// if clicked from tree
var fromTree = $element.closest("#cp-app-drive-tree").length;
if (fromTree) {
removeSelected();
}
// if clicked on non selected element
if (!isElementSelected($element)) {
removeSelected();
}
if (type === 'trash' && !$element.data('path')) { return; } if (type === 'trash' && !$element.data('path')) { return; }
if (!$element.length) { if (!$element.length) {
@ -1401,8 +1463,8 @@ define([
return false; return false;
} }
if (!$element.hasClass('cp-app-drive-element-selected')) { if (!isElementSelected($element)) {
onElementClick(undefined, $element); selectElement($element);
} }
paths = getSelectedPaths($element); paths = getSelectedPaths($element);
@ -1472,6 +1534,7 @@ define([
if (!res) { return; } if (!res) { return; }
manager.delete(pathsList, function () { manager.delete(pathsList, function () {
pathsList.forEach(removeFoldersOpened); pathsList.forEach(removeFoldersOpened);
removeSelected();
refresh(); refresh();
}); });
}, null, true); }, null, true);
@ -1482,7 +1545,7 @@ define([
var paths = []; var paths = [];
var $element = findDataHolder($(ev.target)); var $element = findDataHolder($(ev.target));
if ($element.hasClass('cp-app-drive-element-selected')) { if ($element.hasClass('cp-app-drive-element-selected')) {
var $selected = $('.cp-app-drive-element-selected'); var $selected = findSelectedElements();
$selected.each(function (idx, elmt) { $selected.each(function (idx, elmt) {
var ePath = $(elmt).data('path'); var ePath = $(elmt).data('path');
if (ePath) { if (ePath) {
@ -1499,7 +1562,7 @@ define([
}); });
} else { } else {
removeSelected(); removeSelected();
$element.addClass('cp-app-drive-element-selected'); selectElement($element);
var val = manager.find(path); var val = manager.find(path);
if (!val) { return; } // The element is not in the object if (!val) { return; } // The element is not in the object
paths = [{ paths = [{
@ -1518,7 +1581,13 @@ define([
var findDropPath = function (target) { var findDropPath = function (target) {
var $target = $(target); var $target = $(target);
var $el = findDataHolder($target); var $el;
if ($target.is(".cp-app-drive-path-element")) {
$el = $target;
}
else {
$el = findDataHolder($target);
}
var newPath = $el.data('path'); var newPath = $el.data('path');
var dropEl = newPath && manager.find(newPath); var dropEl = newPath && manager.find(newPath);
if (newPath && manager.isSharedFolder(dropEl)) { if (newPath && manager.isSharedFolder(dropEl)) {
@ -1646,7 +1715,8 @@ define([
$owner.attr('title', Messages.fm_padIsOwnedOther); $owner.attr('title', Messages.fm_padIsOwnedOther);
} }
}; };
var addFileData = function (element, $span) { var thumbsUrls = {};
var addFileData = function (element, $element) {
if (!manager.isFile(element)) { return; } if (!manager.isFile(element)) { return; }
var data = manager.getFileData(element); var data = manager.getFileData(element);
@ -1655,7 +1725,7 @@ define([
var hrefData = Hash.parsePadUrl(href); var hrefData = Hash.parsePadUrl(href);
if (hrefData.type) { if (hrefData.type) {
$span.addClass('cp-border-color-'+hrefData.type); $element.addClass('cp-border-color-'+hrefData.type);
} }
var $state = $('<span>', {'class': 'cp-app-drive-element-state'}); var $state = $('<span>', {'class': 'cp-app-drive-element-state'});
@ -1675,25 +1745,38 @@ define([
var $expire = $expirableIcon.clone().appendTo($state); var $expire = $expirableIcon.clone().appendTo($state);
$expire.attr('title', Messages._getKey('fm_expirablePad', [new Date(data.expire).toLocaleString()])); $expire.attr('title', Messages._getKey('fm_expirablePad', [new Date(data.expire).toLocaleString()]));
} }
_addOwnership($span, $state, data); _addOwnership($element, $state, data);
var name = manager.getTitle(element); var name = manager.getTitle(element);
// The element with the class '.name' is underlined when the 'li' is hovered // The element with the class '.name' is underlined when the 'li' is hovered
var $name = $('<span>', {'class': 'cp-app-drive-element-name'}).text(name); var $name = $('<span>', {'class': 'cp-app-drive-element-name'}).text(name);
$span.append($name); $element.append($name);
$span.append($state); $element.append($state);
$span.attr('title', name); $element.attr('title', name);
// display the thumbnail
// if the thumbnail has already been displayed once, do not reload it, keep the same url
if (thumbsUrls[element]) {
var img = new Image();
img.src = thumbsUrls[element];
$element.find('.cp-icon').addClass('cp-app-drive-element-list');
$element.prepend(img);
$(img).addClass('cp-app-drive-element-grid cp-app-drive-element-thumbnail');
$(img).attr("draggable", false);
}
else {
common.displayThumbnail(href || data.roHref, data.channel, data.password, $element, function ($thumb) {
// Called only if the thumbnail exists
// Remove the .hide() added by displayThumnail() because it hides the icon in list mode too
$element.find('.cp-icon').removeAttr('style').addClass('cp-app-drive-element-list');
$thumb.addClass('cp-app-drive-element-grid cp-app-drive-element-thumbnail');
$thumb.attr("draggable", false);
thumbsUrls[element] = $thumb[0].src;
});
}
var type = Messages.type[hrefData.type] || hrefData.type; var type = Messages.type[hrefData.type] || hrefData.type;
common.displayThumbnail(href || data.roHref, data.channel, data.password, $span, function ($thumb) {
// Called only if the thumbnail exists
// Remove the .hide() added by displayThumnail() because it hides the icon in
// list mode too
$span.find('.cp-icon').removeAttr('style').addClass('cp-app-drive-element-list');
$thumb.addClass('cp-app-drive-element-grid')
.addClass('cp-app-drive-element-thumbnail');
});
var $type = $('<span>', { var $type = $('<span>', {
'class': 'cp-app-drive-element-type cp-app-drive-element-list' 'class': 'cp-app-drive-element-type cp-app-drive-element-list'
}).text(type); }).text(type);
@ -1703,7 +1786,7 @@ define([
var $cdate = $('<span>', { var $cdate = $('<span>', {
'class': 'cp-app-drive-element-ctime cp-app-drive-element-list' 'class': 'cp-app-drive-element-ctime cp-app-drive-element-list'
}).text(getDate(data.ctime)); }).text(getDate(data.ctime));
$span.append($type).append($adate).append($cdate); $element.append($type).append($adate).append($cdate);
}; };
var addFolderData = function (element, key, $span) { var addFolderData = function (element, key, $span) {
@ -1779,12 +1862,9 @@ define([
draggable: true, draggable: true,
'class': 'cp-app-drive-element-row' 'class': 'cp-app-drive-element-row'
}); });
if (!isFolder && Array.isArray(APP.selectedFiles)) { $element.data('path', newPath);
var idx = APP.selectedFiles.indexOf(element); if (isElementSelected($element)) {
if (idx !== -1) { selectElement($element);
$element.addClass('cp-app-drive-element-selected');
APP.selectedFiles.splice(idx, 1);
}
} }
$element.prepend($icon).dblclick(function () { $element.prepend($icon).dblclick(function () {
if (isFolder) { if (isFolder) {
@ -1800,11 +1880,10 @@ define([
addFileData(element, $element); addFileData(element, $element);
} }
$element.addClass(liClass); $element.addClass(liClass);
$element.data('path', newPath);
addDragAndDropHandlers($element, newPath, isFolder, !isTrash); addDragAndDropHandlers($element, newPath, isFolder, !isTrash);
$element.click(function(e) { $element.click(function(e) {
e.stopPropagation(); e.stopPropagation();
onElementClick(e, $element, newPath); onElementClick(e, $element);
}); });
if (!isTrash) { if (!isTrash) {
$element.contextmenu(openContextMenu('tree')); $element.contextmenu(openContextMenu('tree'));
@ -1955,6 +2034,8 @@ define([
} else if (idx > 0 && manager.isFile(el)) { } else if (idx > 0 && manager.isFile(el)) {
name = getElementName(path); name = getElementName(path);
} }
$span.data("path", path.slice(0, idx + 1));
addDragAndDropHandlers($span, path.slice(0, idx), true, true);
if (idx === 0) { name = p === SHARED_FOLDER ? name : getPrettyName(p); } if (idx === 0) { name = p === SHARED_FOLDER ? name : getPrettyName(p); }
else { else {
@ -2265,15 +2346,22 @@ define([
var data = manager.getSharedFolderData(id); var data = manager.getSharedFolderData(id);
var parsed = Hash.parsePadUrl(data.href); var parsed = Hash.parsePadUrl(data.href);
if (!parsed || !parsed.hash) { return void console.error("Invalid href: "+data.href); } if (!parsed || !parsed.hash) { return void console.error("Invalid href: "+data.href); }
var friends = common.getFriends();
var modal = UIElements.createSFShareModal({ var modal = UIElements.createSFShareModal({
origin: APP.origin, origin: APP.origin,
pathname: "/drive/", pathname: "/drive/",
friends: friends,
title: data.title,
password: data.password,
common: common,
hashes: { hashes: {
editHash: parsed.hash editHash: parsed.hash
} }
}); });
$shareBlock.click(function () { $shareBlock.click(function () {
UI.openCustomModal(modal); UI.openCustomModal(modal, {
wide: Object.keys(friends).length !== 0
});
}); });
$container.append($shareBlock); $container.append($shareBlock);
}; };
@ -2592,22 +2680,19 @@ define([
'class': 'cp-app-drive-element cp-app-drive-element-file cp-app-drive-element-row' + roClass, 'class': 'cp-app-drive-element cp-app-drive-element-file cp-app-drive-element-row' + roClass,
draggable: draggable draggable: draggable
}); });
if (Array.isArray(APP.selectedFiles)) {
var sidx = APP.selectedFiles.indexOf(id); var path = [rootName, idx];
if (sidx !== -1) { $element.data('path', path);
$element.addClass('cp-app-drive-element-selected'); if (isElementSelected($element)) {
APP.selectedFiles.splice(sidx, 1); selectElement($element);
}
} }
$element.prepend($icon).dblclick(function () { $element.prepend($icon).dblclick(function () {
openFile(id); openFile(id);
}); });
addFileData(id, $element); addFileData(id, $element);
var path = [rootName, idx];
$element.data('path', path);
$element.click(function(e) { $element.click(function(e) {
e.stopPropagation(); e.stopPropagation();
onElementClick(e, $element, path); onElementClick(e, $element);
}); });
$element.contextmenu(openContextMenu('default')); $element.contextmenu(openContextMenu('default'));
$element.data('context', 'default'); $element.data('context', 'default');
@ -2734,8 +2819,8 @@ define([
e.preventDefault(); e.preventDefault();
if (manager.isInTrashRoot(parentPath)) { parentPath = [TRASH]; } if (manager.isInTrashRoot(parentPath)) { parentPath = [TRASH]; }
else { parentPath.pop(); } else { parentPath.pop(); }
APP.selectedFiles = [r.id];
APP.displayDirectory(parentPath); APP.displayDirectory(parentPath);
APP.selectedFiles = path.slice(-1);
}).appendTo($openDir); }).appendTo($openDir);
} }
$('<a>').text(Messages.fc_prop).click(function () { $('<a>').text(Messages.fc_prop).click(function () {
@ -2748,7 +2833,7 @@ define([
else { else {
$icon.append($folderIcon.clone()); $icon.append($folderIcon.clone());
$type.text(Messages.fm_folder); $type.text(Messages.fm_folder);
$('<a>').text(Messages.fm_OpenFolder || "Open folder").click(function (e) { $('<a>').text(Messages.fc_open).click(function (e) {
e.preventDefault(); e.preventDefault();
APP.displayDirectory(path); APP.displayDirectory(path);
}).appendTo($openDir); }).appendTo($openDir);
@ -2826,7 +2911,7 @@ define([
$element.data('path', path); $element.data('path', path);
$element.click(function(e) { $element.click(function(e) {
e.stopPropagation(); e.stopPropagation();
onElementClick(e, $element, path); onElementClick(e, $element);
}); });
$element.contextmenu(openContextMenu('default')); $element.contextmenu(openContextMenu('default'));
$element.data('context', 'default'); $element.data('context', 'default');
@ -3050,7 +3135,7 @@ define([
$context.click(function (e) { $context.click(function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
var $li = $content.find('.cp-app-drive-element-selected'); var $li = findSelectedElements();
if ($li.length !== 1) { if ($li.length !== 1) {
$li = findDataHolder($tree.find('.cp-app-drive-element-active')); $li = findDataHolder($tree.find('.cp-app-drive-element-active'));
} }
@ -3060,11 +3145,6 @@ define([
return; return;
} }
// Open the menu // Open the menu
$('.cp-contextmenu').css({
top: ($context.offset().top + 32) + 'px',
right: '0px',
left: ''
});
$li.contextmenu(); $li.contextmenu();
}); });
} else { } else {
@ -3132,7 +3212,7 @@ define([
} }
});*/ });*/
var $sel = $content.find('.cp-app-drive-element-selected'); var $sel = findSelectedElements();
if ($sel.length) { if ($sel.length) {
$sel[0].scrollIntoView(); $sel[0].scrollIntoView();
} else { } else {
@ -3144,6 +3224,9 @@ define([
if (history.isHistoryMode) { if (history.isHistoryMode) {
return void _displayDirectory(path, force); return void _displayDirectory(path, force);
} }
if (!manager.comparePath(currentPath, path)) {
removeSelected();
}
updateObject(sframeChan, proxy, function () { updateObject(sframeChan, proxy, function () {
copyObjectValue(files, proxy.drive); copyObjectValue(files, proxy.drive);
updateSharedFolders(sframeChan, manager, files, folders, function () { updateSharedFolders(sframeChan, manager, files, folders, function () {
@ -3470,7 +3553,9 @@ define([
//data.noPassword = true; //data.noPassword = true;
data.noEditPassword = true; data.noEditPassword = true;
data.noExpiration = true; data.noExpiration = true;
data.sharedFolder = true; // XXX debug // this is here to allow users to check the channel id of a shared folder
// we should remove it at some point
data.sharedFolder = true;
} }
UIElements.getProperties(common, data, cb); UIElements.getProperties(common, data, cb);
@ -3492,6 +3577,7 @@ define([
if (!res) { return; } if (!res) { return; }
manager.delete(pathsList, function () { manager.delete(pathsList, function () {
pathsList.forEach(removeFoldersOpened); pathsList.forEach(removeFoldersOpened);
removeSelected();
refresh(); refresh();
}); });
}); });
@ -3608,6 +3694,7 @@ define([
friends: friends, friends: friends,
title: data.title, title: data.title,
common: common, common: common,
password: data.password,
hashes: { hashes: {
editHash: parsed.hash editHash: parsed.hash
} }
@ -3621,6 +3708,7 @@ define([
origin: APP.origin, origin: APP.origin,
pathname: "/" + padType + "/", pathname: "/" + padType + "/",
friends: friends, friends: friends,
password: data.password,
hashes: { hashes: {
editHash: parsed.hash, editHash: parsed.hash,
viewHash: roParsed.hash, viewHash: roParsed.hash,
@ -3630,6 +3718,7 @@ define([
hash: parsed.hash, hash: parsed.hash,
password: data.password password: data.password
}, },
isTemplate: paths[0].path[0] === 'template',
title: data.title, title: data.title,
common: common common: common
}; };
@ -3735,16 +3824,15 @@ define([
var parentPath = paths[0].path.slice(); var parentPath = paths[0].path.slice();
if (manager.isInTrashRoot(parentPath)) { parentPath = [TRASH]; } if (manager.isInTrashRoot(parentPath)) { parentPath = [TRASH]; }
else { parentPath.pop(); } else { parentPath.pop(); }
el = manager.find(paths[0].path);
APP.selectedFiles = [el];
APP.displayDirectory(parentPath); APP.displayDirectory(parentPath);
APP.selectedFiles = paths[0].path.slice(-1);
} }
APP.hideMenu(); APP.hideMenu();
}); });
$content.on("keydown", function (e) { $(window).on("keydown", function (e) {
if (e.which === 113) { if (e.which === 113) { // if F2 key pressed
var paths = $contextMenu.data('paths'); var paths = getSelectedPaths(findSelectedElements().first());
if (paths.length !== 1) { return; } if (paths.length !== 1) { return; }
displayRenameInput(paths[0].element, paths[0].path); displayRenameInput(paths[0].element, paths[0].path);
} }
@ -3756,11 +3844,9 @@ define([
e.preventDefault(); e.preventDefault();
}); });
$appContainer.on('mouseup', function (e) { $appContainer.on('mouseup', function (e) {
//if (sel.down) { return; }
if (e.which !== 1) { return ; } if (e.which !== 1) { return ; }
if ($(e.target).is(".dropdown-submenu a, .dropdown-submenu a span")) { return; } // if we click on dropdown-submenu, don't close menu if ($(e.target).is(".dropdown-submenu a, .dropdown-submenu a span")) { return; } // if we click on dropdown-submenu, don't close menu
APP.hideMenu(e); APP.hideMenu(e);
//removeSelected(e);
}); });
$appContainer.on('click', function (e) { $appContainer.on('click', function (e) {
if (e.which !== 1) { return ; } if (e.which !== 1) { return ; }
@ -3779,7 +3865,7 @@ define([
if (manager.isPathIn(currentPath, [FILES_DATA]) && APP.loggedIn) { if (manager.isPathIn(currentPath, [FILES_DATA]) && APP.loggedIn) {
return; // We can't remove elements directly from filesData return; // We can't remove elements directly from filesData
} }
var $selected = $('.cp-app-drive-element-selected'); var $selected = findSelectedElements();
if (!$selected.length) { return; } if (!$selected.length) { return; }
var paths = []; var paths = [];
var isTrash = manager.isPathIn(currentPath, [TRASH]); var isTrash = manager.isPathIn(currentPath, [TRASH]);

@ -52,6 +52,16 @@
max-width: 100%; max-width: 100%;
max-height: ~"calc(100vh - 96px)"; max-height: ~"calc(100vh - 96px)";
} }
.plain-text-reader {
align-self: flex-start;
width: 90vw;
height: 100%;
padding: 2em;
background-color: white;
overflow-y: auto;
word-wrap: break-word;
white-space: pre-wrap;
}
} }
#cp-app-file-upload-form, #cp-app-file-download-form { #cp-app-file-upload-form, #cp-app-file-download-form {

@ -123,7 +123,10 @@ define([
common.setPadAttribute('fileType', metadata.type); common.setPadAttribute('fileType', metadata.type);
} }
toolbar.addElement(['pageTitle'], {pageTitle: title}); toolbar.addElement(['pageTitle'], {
pageTitle: title,
title: Title.getTitleConfig(),
});
toolbar.$rightside.append(common.createButton('forget', true)); toolbar.$rightside.append(common.createButton('forget', true));
toolbar.$rightside.append(common.createButton('properties', true)); toolbar.$rightside.append(common.createButton('properties', true));
if (common.isLoggedIn()) { if (common.isLoggedIn()) {

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html class="cp">
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
<head>
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/dialog/dialog.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/fold/foldgutter.css" />
</head>
<body class="html">
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</html>

@ -1,88 +0,0 @@
define([
'jquery',
'/common/cryptpad-common.js',
'/common/common-interface.js',
//'/common/common-hash.js',
//'/bower_components/chainpad-listmap/chainpad-listmap.js',
//'/common/curve.js',
'less!/invite/main.less',
], function ($, Cryptpad, UI/*, Hash , Listmap, Curve*/) {
var Messages = Cryptpad.Messages;
var comingSoon = function () {
return $('<div>', {
'class': 'coming-soon',
})
.text(Messages.comingSoon)
.append('<br>');
};
$(function () {
UI.removeLoadingScreen();
console.log("wut");
$('body #mainBlock').append(comingSoon());
});
return;
/* jshint ignore:start */
var APP = window.APP = {};
//var Messages = Cryptpad.Messages;
var onInit = function () {};
var onDisconnect = function () {};
var onChange = function () {};
var andThen = function () {
var hash = window.location.hash.slice(1);
var info = Hash.parseTypeHash('invite', hash);
console.log(info);
if (!info.pubkey) {
UI.removeLoadingScreen();
UI.alert('invalid invite');
return;
}
var proxy = Cryptpad.getProxy();
var mySecret = proxy.curvePrivate;
var keys = Curve.deriveKeys(info.pubkey, mySecret);
var encryptor = Curve.createEncryptor(keys);
UI.removeLoadingScreen();
var listmapConfig = {
data: {},
network: Cryptpad.getNetwork(),
channel: info.channel,
readOnly: false,
validateKey: keys.validateKey,
crypto: encryptor,
userName: 'profile',
logLevel: 1,
};
var lm = APP.lm = Listmap.create(listmapConfig);
lm.proxy.on('create', onInit)
.on('ready', function () {
APP.initialized = true;
console.log(JSON.stringify(lm.proxy));
})
.on('disconnect', onDisconnect)
.on('change', [], onChange);
};
$(function () {
var $main = $('#mainBlock');
// main block is hidden in case javascript is disabled
$main.removeClass('hidden');
APP.$container = $('#container');
Cryptpad.ready(function () {
andThen();
});
});
/* jshint ignore:end */
});

@ -1,150 +0,0 @@
/*
.cp {
#mainBlock {
z-index: 1;
width: 1000px;
max-width: 90%;
margin: auto;
#container {
font-size: 25px;
width: 100%;
}
}
#header {
display: flex;
#rightside {
flex: 1;
display: flex;
flex-flow: column;
}
}
#avatar {
width: 300px;
//height: 350px;
margin: 10px;
margin-right: 20px;
text-align: center;
&> span {
display: inline-block;
text-align: center;
height: 300px;
width: 300px;
border: 1px solid black;
border-radius: 10px;
overflow: hidden;
position: relative;
.delete {
right: 0;
position: absolute;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
}
img {
max-width: 100%;
max-height: 100%;
vertical-align: top;
}
media-tag {
height: 100%;
width: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
img {
min-width: 100%;
min-height: 100%;
max-width: none;
max-height: none;
flex-shrink: 0;
}
}
button {
height: 40px;
margin: 5px;
}
}
#displayName, #link {
width: 100%;
height: 40px;
margin: 10px 0;
input {
width: 100%;
font-size: 20px;
box-sizing: border-box;
padding-right: 30px;
}
input:focus ~ .edit {
display: none;
}
.edit {
position: absolute;
margin-left: -25px;
margin-top: 8px;
}
.temp {
font-weight: 400;
font-family: sans-serif;
}
.displayName {
font-weight: bold;
font-size: 30px;
}
.displayName, .link {
line-height: 40px;
}
}
#description {
position: relative;
font-size: 16px;
border: 1px solid #DDD;
margin-bottom: 20px;
.rendered {
padding: 0 15px;
}
.ok, .spin {
position: absolute;
top: 2px;
right: 2px;
display: none;
z-index: 1000;
}
textarea {
width: 100%;
height: 300px;
}
.CodeMirror {
border: 1px solid #DDD;
font-family: monospace;
font-size: 16px;
line-height: initial;
pre {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
}
}
#createProfile {
height: 100%;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
}
}
*/
.coming-soon {
text-align: center;
font-size: 25px;
height: 100%;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
}

@ -2,7 +2,9 @@
// Pads from the code app will be exported using this format instead of plain text. // Pads from the code app will be exported using this format instead of plain text.
define([ define([
], function () { ], function () {
var module = {}; var module = {
ext: '.json'
};
module.main = function (userDoc, cb) { module.main = function (userDoc, cb) {
var content = userDoc.content; var content = userDoc.content;

@ -367,7 +367,7 @@ define([
}); });
} }
framework.setFileExporter('json', function () { framework.setFileExporter('.json', function () {
return new Blob([JSON.stringify(kanban.getBoardsJSON(), 0, 2)], { return new Blob([JSON.stringify(kanban.getBoardsJSON(), 0, 2)], {
type: 'application/json', type: 'application/json',
}); });

@ -0,0 +1,142 @@
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
&.cp-app-notifications {
.framework_min_main(
@bg-color: @colortheme_notifications-bg,
@warn-color: @colortheme_notifications-warn,
@color: @colortheme_notifications-color
);
.sidebar-layout_main();
display: flex;
flex-flow: column;
.cp-clickable {
cursor: pointer;
&:hover {
background-color: rgba(0,0,0,0.1);
}
}
.cp-app-notifications-panel {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
width: 100%;
.cp-app-notifications-panel-titlebar {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
// border-radius: 5px 5px 0 0;
background-color: #888;
color: #fff;
.cp-app-notifications-panel-title {
flex-grow: 1;
margin: 1rem 1rem;
}
.cp-app-notifications-panel-titlebar-buttons {
align-self: stretch;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: stretch;
.cp-app-notifications-dismissall {
align-self: stretch;
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 3rem;
}
}
}
.cp-app-notifications-panel-list {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
border: 1px solid #ccc;
border-top: none;
// border-radius: 0 0 5px 5px;
overflow: hidden;
.cp-notification {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
background-color: #ffffff;
&.no-notifications {
display: none;
padding: 1rem 1rem;
font-style: italic;
color: #777;
&:only-child {
display: block;
}
}
&.cp-app-notification-archived {
background-color: #f1f1f1;
}
&:not(:first-child) {
border-top: 1px solid #ccc;
}
&.dismissed {
display: none;
}
.cp-notification-content {
flex-grow: 1;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
p {
display: inline-block;
margin: 1rem 1rem;
}
.notification-time {
margin: 1rem 1rem;
color: grey;
margin-left: auto;
}
}
.cp-notification-dismiss {
align-self: stretch;
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-left: 1px solid #ccc;
width: 3rem;
}
}
}
}
.cp-app-notification-loadmore {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
color: #333;
}
}

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html class="cp-app-noscroll">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/notifications/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; }
</style>
</head>
<body class="cp-app-notifications">
<div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-sidebarlayout-container"></div>
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</body>

@ -0,0 +1,265 @@
define([
'jquery',
'/api/config',
'/bower_components/chainpad-crypto/crypto.js',
'/common/toolbar3.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/hyperscript.js',
'/customize/messages.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!/notifications/app-notifications.less',
], function (
$,
ApiConfig,
Crypto,
Toolbar,
nThen,
SFCommon,
h,
Messages,
UI
)
{
var APP = {};
var common;
var sFrameChan;
var categories = {
'all': [
'cp-notifications-all',
],
'friends': [
'cp-notifications-friends',
],
'pads': [
'cp-notifications-pads',
],
'archived': [
'cp-notifications-archived',
],
};
var notifsAllowedTypes = ["FRIEND_REQUEST", "FRIEND_REQUEST_ACCEPTED", "FRIEND_REQUEST_DECLINED", "SHARE_PAD", "REQUEST_PAD_ACCESS"];
var create = {};
var unreadData;
// create the list of notifications
// show only notifs with type in filterTypes array
var makeNotificationList = function (key, filterTypes) {
filterTypes = filterTypes || [];
var safeKey = key.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
var categoryName = Messages['notifications_cat_' + safeKey] || safeKey;
var notifsData = [];
if (key === "all") {
unreadData = notifsData;
}
var $div = $('<div>', {'class': 'cp-notifications-' + key + ' cp-sidebarlayout-element'});
var notifsPanel, notifsList, dismissAll;
notifsPanel = h("div.cp-app-notifications-panel", [
h('div.cp-app-notifications-panel-titlebar', [
h("span.cp-app-notifications-panel-title",
(Messages.notificationsPage || "Notifications") + " - " + categoryName),
h("div.cp-app-notifications-panel-titlebar-buttons", [
dismissAll = h("div.cp-app-notifications-dismissall.cp-clickable", { title: Messages.notifications_dismissAll || "Dismiss All" }, h("span.fa.fa-trash")),
]),
]),
notifsList = h("div.cp-app-notifications-panel-list", [
h("div.cp-notification.no-notifications", Messages.notifications_empty),
]),
]);
// add notification
var addNotification = function (data, el) {
// if the type of notification correspond
if (filterTypes.indexOf(data.content.msg.type) !== -1) {
notifsData.push(data);
$(notifsList).prepend(el);
}
};
var addArchivedNotification = function (data) {
// if the type is allowed
if (data.content.archived && notifsAllowedTypes.indexOf(data.content.msg.type) !== -1) {
var isDataUnread = unreadData.some(function (ud) {
return ud.content.hash === data.content.hash;
});
notifsData.push(data);
var el = common.mailbox.createElement(data);
var time = new Date(data.content.time);
$(el).find(".cp-notification-content").append(h("span.notification-time", time.toLocaleString()));
$(el).addClass("cp-app-notification-archived");
$(el).toggle(!isDataUnread);
$(notifsList).append(el);
}
};
$div.append(notifsPanel);
if (key === "archived") {
var loadmore;
var lastKnownHash;
$(dismissAll).remove();
loadmore = h("div.cp-app-notification-loadmore.cp-clickable", Messages.history_loadMore);
$(loadmore).click(function () {
common.mailbox.getNotificationsHistory('notifications', 10, lastKnownHash, function (err, messages, end) {
if (!Array.isArray(messages)) { return; }
// display archived notifs from most recent to oldest
for (var i = messages.length - 1 ; i >= 0 ; i--) {
var data = messages[i];
data.content.archived = true;
addArchivedNotification(data);
}
if (end) {
$(loadmore).hide();
}
else {
lastKnownHash = messages[0].content.hash;
}
$('#cp-sidebarlayout-rightside').scrollTop($('#cp-sidebarlayout-rightside')[0].scrollHeight);
});
});
notifsList.after(loadmore);
$(loadmore).click();
}
common.mailbox.subscribe(["notifications"], {
onMessage: function (data, el) {
addNotification(data, el);
},
onViewed: function (data) {
$('.cp-app-notification-archived[data-hash="' + data.hash + '"]').show();
}
});
$(dismissAll).click(function () {
notifsData.forEach(function (data) {
if (data.content.isDismissible) {
data.content.dismissHandler();
}
});
});
return $div;
};
create['all'] = function () {
var key = 'all';
return makeNotificationList(key, notifsAllowedTypes);
};
create['friends'] = function () {
var key = 'friends';
var filter = ["FRIEND_REQUEST", "FRIEND_REQUEST_ACCEPTED", "FRIEND_REQUEST_DECLINED"];
return makeNotificationList(key, filter);
};
create['pads'] = function () {
var key = 'pads';
var filter = ["SHARE_PAD"];
return makeNotificationList(key, filter);
};
create['archived'] = function () {
var key = 'archived';
var filter = [];
return makeNotificationList(key, filter);
};
var hideCategories = function () {
APP.$rightside.find('> div').hide();
};
var showCategories = function (cat) {
hideCategories();
cat.forEach(function (c) {
APP.$rightside.find('.'+c).show();
});
};
var createLeftside = function () {
var $categories = $('<div>', {'class': 'cp-sidebarlayout-categories'})
.appendTo(APP.$leftside);
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var active = privateData.category || 'all';
common.setHash(active);
Object.keys(categories).forEach(function (key) {
var $category = $('<div>', {'class': 'cp-sidebarlayout-category'}).appendTo($categories);
if (key === 'all') { $category.append($('<span>', {'class': 'fa fa-bars'})); }
if (key === 'friends') { $category.append($('<span>', {'class': 'fa fa-user'})); }
if (key === 'pads') { $category.append($('<span>', {'class': 'cptools cptools-pad'})); }
if (key === 'archived') { $category.append($('<span>', {'class': 'fa fa-archive'})); }
if (key === active) {
$category.addClass('cp-leftside-active');
}
$category.click(function () {
if (!Array.isArray(categories[key]) && categories[key].onClick) {
categories[key].onClick();
return;
}
active = key;
common.setHash(key);
$categories.find('.cp-leftside-active').removeClass('cp-leftside-active');
$category.addClass('cp-leftside-active');
showCategories(categories[key]);
});
$category.append(Messages['notifications_cat_'+key] || key);
});
showCategories(categories[active]);
};
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
var configTb = {
displayed: displayed,
sfCommon: common,
$container: APP.$toolbar,
pageTitle: Messages.notificationsPage || 'Notifications',
metadataMgr: common.getMetadataMgr(),
};
APP.toolbar = Toolbar.create(configTb);
APP.toolbar.$rightside.hide();
};
nThen(function (waitFor) {
$(waitFor(UI.addLoadingScreen));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (waitFor) {
APP.$container = $('#cp-sidebarlayout-container');
APP.$toolbar = $('#cp-toolbar');
APP.$leftside = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);
APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container);
sFrameChan = common.getSframeChannel();
sFrameChan.onReady(waitFor());
}).nThen(function (/*waitFor*/) {
createToolbar();
common.setTabTitle(Messages.notificationsPage || 'Notifications');
// Content
var $rightside = APP.$rightside;
var addItem = function (cssClass) {
var item = cssClass.slice(17); // remove 'cp-notifications-'
if (typeof (create[item]) === "function") {
$rightside.append(create[item]());
}
};
for (var cat in categories) {
if (!Array.isArray(categories[cat])) { continue; }
categories[cat].forEach(addItem);
}
createLeftside();
UI.removeLoadingScreen();
});
});

@ -0,0 +1,52 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'/common/dom-ready.js',
'/common/requireconfig.js',
'/common/sframe-common-outer.js',
], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) {
var requireConfig = RequireConfig();
// Loaded in load #2
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
document.getElementById('sbox-iframe').setAttribute('src',
ApiConfig.httpSafeOrigin + '/notifications/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 category;
if (window.location.hash) {
category = window.location.hash.slice(1);
window.location.hash = '';
}
var addData = function (obj) {
if (category) { obj.category = category; }
};
SFCommonO.start({
noRealtime: true,
addData: addData
});
});
});

@ -5,7 +5,7 @@ define([
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
], function ($, Util, Hyperjson, nThen) { ], function ($, Util, Hyperjson, nThen) {
var module = { var module = {
type: 'html' ext: '.html'
}; };
var exportMediaTags = function (inner, cb) { var exportMediaTags = function (inner, cb) {

@ -786,7 +786,7 @@ define([
}); });
}, true); }, true);
framework.setFileExporter(Exporter.type, function (cb) { framework.setFileExporter(Exporter.ext, function (cb) {
Exporter.main(inner, cb); Exporter.main(inner, cb);
}, true); }, true);

@ -3,7 +3,9 @@
define([ define([
'/customize/messages.js', '/customize/messages.js',
], function (Messages) { ], function (Messages) {
var module = {}; var module = {
ext: '.csv'
};
var copyObject = function (obj) { var copyObject = function (obj) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));

@ -559,7 +559,7 @@ define([
lm.proxy.on('ready', function () { lm.proxy.on('ready', function () {
updateValues(lm.proxy); updateValues(lm.proxy);
UI.removeLoadingScreen(); UI.removeLoadingScreen();
common.mailbox.subscribe({ common.mailbox.subscribe(["notifications"], {
onMessage: function () { onMessage: function () {
refreshFriendRequest(lm.proxy); refreshFriendRequest(lm.proxy);
}, },

@ -111,8 +111,14 @@
vertical-align: middle; vertical-align: middle;
margin-right: 5px; margin-right: 5px;
} }
input[type="color"] { .cp-settings-cursor-color-picker {
width: 100px; display: inline-block;
vertical-align: middle;
height: 25px;
width: 70px;
margin-right: 10px;
cursor: pointer;
border: 1px solid black;
} }
.cp-settings-language-selector { .cp-settings-language-selector {
button.btn { button.btn {

@ -15,6 +15,7 @@ define([
'/settings/make-backup.js', '/settings/make-backup.js',
'/common/common-feedback.js', '/common/common-feedback.js',
'/common/jscolor.js',
'/bower_components/file-saver/FileSaver.min.js', '/bower_components/file-saver/FileSaver.min.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
@ -1191,13 +1192,13 @@ define([
var $inputBlock = $('<div>').appendTo($div); var $inputBlock = $('<div>').appendTo($div);
var $colorPicker = $("<div>", { class: "cp-settings-cursor-color-picker"});
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}); var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved});
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}); var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'});
var $input = $('<input>', { // when jscolor picker value change
type: 'color', var onchange = function (colorL) {
}).on('change', function () { var val = "#" + colorL.toString();
var val = $input.val();
if (!/^#[0-9a-fA-F]{6}$/.test(val)) { return; } if (!/^#[0-9a-fA-F]{6}$/.test(val)) { return; }
$spinner.show(); $spinner.show();
$ok.hide(); $ok.hide();
@ -1205,15 +1206,25 @@ define([
$spinner.hide(); $spinner.hide();
$ok.show(); $ok.show();
}); });
}).appendTo($inputBlock); };
$ok.hide().appendTo($inputBlock); // jscolor picker
$spinner.hide().appendTo($inputBlock); var jscolorL = new window.jscolor($colorPicker[0],{showOnClick: false, onFineChange: onchange, valueElement:undefined});
$colorPicker.click(function () {
jscolorL.show();
});
// set default color
common.getAttribute(['general', 'cursor', 'color'], function (e, val) { common.getAttribute(['general', 'cursor', 'color'], function (e, val) {
if (e) { return void console.error(e); } if (e) { return void console.error(e); }
$input.val(val || ''); val = val || "#000";
jscolorL.fromString(val);
}); });
$colorPicker.appendTo($inputBlock);
$ok.hide().appendTo($inputBlock);
$spinner.hide().appendTo($inputBlock);
return $div; return $div;
}; };

@ -34,7 +34,7 @@ define([
var path = '/' + type + '/export.js'; var path = '/' + type + '/export.js';
require([path], function (Exporter) { require([path], function (Exporter) {
Exporter.main(json, function (data) { Exporter.main(json, function (data) {
result.ext = '.' + Exporter.type; result.ext = Exporter.ext || '';
result.data = data; result.data = data;
cb(result); cb(result);
}); });
@ -163,12 +163,12 @@ define([
var existingNames = []; var existingNames = [];
Object.keys(root).forEach(function (k) { Object.keys(root).forEach(function (k) {
var el = root[k]; var el = root[k];
if (typeof el === "object") { if (typeof el === "object" && el.metadata !== true) { // if folder
var fName = getUnique(sanitize(k), '', existingNames); var fName = getUnique(sanitize(k), '', existingNames);
existingNames.push(fName.toLowerCase()); existingNames.push(fName.toLowerCase());
return void makeFolder(ctx, el, zip.folder(fName), fd); return void makeFolder(ctx, el, zip.folder(fName), fd);
} }
if (ctx.data.sharedFolders[el]) { if (ctx.data.sharedFolders[el]) { // if shared folder
var sfData = ctx.sf[el].metadata; var sfData = ctx.sf[el].metadata;
var sfName = getUnique(sanitize(sfData.title || 'Folder'), '', existingNames); var sfName = getUnique(sanitize(sfData.title || 'Folder'), '', existingNames);
existingNames.push(sfName.toLowerCase()); existingNames.push(sfName.toLowerCase());

@ -17,7 +17,11 @@ define([
{ {
var APP = window.APP = {}; var APP = window.APP = {};
var init = false;
var andThen = function (common) { var andThen = function (common) {
if (init) { return; }
init = true;
var metadataMgr = common.getMetadataMgr(); var metadataMgr = common.getMetadataMgr();
var sframeChan = common.getSframeChannel(); var sframeChan = common.getSframeChannel();
@ -38,6 +42,8 @@ define([
var modal = f({ var modal = f({
origin: origin, origin: origin,
pathname: pathname, pathname: pathname,
password: priv.password,
isTemplate: priv.isTemplate,
hashes: hashes, hashes: hashes,
common: common, common: common,
title: data.title, title: data.title,
@ -50,7 +56,7 @@ define([
password: priv.password password: priv.password
} }
}); });
UI.findCancelButton().click(); $('button.cancel').click(); // Close any existing alertify
UI.openCustomModal(UI.dialog.tabs(modal), { UI.openCustomModal(UI.dialog.tabs(modal), {
wide: Object.keys(friends).length !== 0 wide: Object.keys(friends).length !== 0
}); });

@ -91,6 +91,7 @@ define([
feedbackAllowed: Utils.Feedback.state, feedbackAllowed: Utils.Feedback.state,
hashes: config.data.hashes, hashes: config.data.hashes,
password: config.data.password, password: config.data.password,
isTemplate: config.data.isTemplate,
file: config.data.file, file: config.data.file,
}; };
for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; } for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }

@ -4,7 +4,7 @@ define([
'/common/sframe-common-codemirror.js', '/common/sframe-common-codemirror.js',
], function (SFCodeMirror) { ], function (SFCodeMirror) {
var module = { var module = {
type: 'md' ext: '.md'
}; };
module.main = function (userDoc, cb) { module.main = function (userDoc, cb) {

@ -538,7 +538,7 @@ define([
editor.on('change', framework.localChange); editor.on('change', framework.localChange);
framework.setFileExporter(CodeMirror.getContentExtension, CodeMirror.fileExporter); framework.setFileExporter(".md", CodeMirror.fileExporter);
framework.setFileImporter({}, CodeMirror.fileImporter); framework.setFileImporter({}, CodeMirror.fileImporter);
framework.start(); framework.start();

@ -0,0 +1,21 @@
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
@import (reference) '../../customize/src/less2/include/support.less';
&.cp-app-support {
.framework_min_main(
@bg-color: @colortheme_support-bg,
@warn-color: @colortheme_support-warn,
@color: @colortheme_support-color
);
.sidebar-layout_main();
.support_main();
.cp-hidden {
display: none !important;
}
display: flex;
flex-flow: column;
}

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html class="cp-app-noscroll">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/support/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; }
</style>
</head>
<body class="cp-app-support">
<div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-sidebarlayout-container"></div>
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</body>
</html>

@ -0,0 +1,267 @@
define([
'jquery',
'/common/toolbar3.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/common-util.js',
'/common/common-hash.js',
'/customize/messages.js',
'/common/hyperscript.js',
'/support/ui.js',
'/api/config',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/support/app-support.less',
], function (
$,
Toolbar,
nThen,
SFCommon,
UI,
UIElements,
Util,
Hash,
Messages,
h,
Support,
ApiConfig
)
{
var APP = window.APP = {};
var common;
var metadataMgr;
var privateData;
var categories = {
'tickets': [
'cp-support-list',
],
'new': [
'cp-support-form',
],
};
var supportKey = ApiConfig.supportMailbox;
var supportChannel = Hash.getChannelIdFromKey(supportKey);
if (!supportKey || !supportChannel) {
categories = {
'tickets': [
'cp-support-disabled'
]
};
}
var create = {};
var makeBlock = function (key, addButton) {
// Convert to camlCase for translation keys
var safeKey = key.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
var $div = $('<div>', {'class': 'cp-support-' + key + ' cp-sidebarlayout-element'});
$('<label>').text(Messages['support_'+safeKey+'Title'] || key).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages['support_'+safeKey+'Hint'] || 'Coming soon...').appendTo($div);
if (addButton) {
$('<button>', {
'class': 'btn btn-primary'
}).text(Messages['support_'+safeKey+'Button'] || safeKey).appendTo($div);
}
return $div;
};
// List existing (open?) tickets
create['list'] = function () {
var key = 'list';
var $div = makeBlock(key);
$div.addClass('cp-support-container');
var hashesById = {};
// Register to the "support" mailbox
common.mailbox.subscribe(['support'], {
onMessage: function (data) {
/*
Get ID of the ticket
If we already have a div for this ID
Push the message to the end of the ticket
If it's a new ticket ID
Make a new div for this ID
*/
var msg = data.content.msg;
var hash = data.content.hash;
var content = msg.content;
var id = content.id;
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]');
hashesById[id] = hashesById[id] || [];
if (hashesById[id].indexOf(hash) === -1) {
hashesById[id].push(data);
}
if (msg.type === 'CLOSE') {
// A ticket has been closed by the admins...
if (!$ticket.length) { return; }
$ticket.addClass('cp-support-list-closed');
$ticket.append(APP.support.makeCloseMessage(content, hash));
return;
}
if (msg.type !== 'TICKET') { return; }
if (!$ticket.length) {
$ticket = APP.support.makeTicket($div, content, function () {
var error = false;
hashesById[id].forEach(function (d) {
common.mailbox.dismiss(d, function (err) {
if (err) {
error = true;
console.error(err);
}
});
});
if (!error) { $ticket.remove(); }
});
}
$ticket.append(APP.support.makeMessage(content, hash));
}
});
return $div;
};
// Create a new tickets
create['form'] = function () {
var key = 'form';
var $div = makeBlock(key, true);
var form = APP.support.makeForm();
$div.find('button').before(form);
var id = Util.uid();
$div.find('button').click(function () {
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var user = metadataMgr.getUserData();
var sent = APP.support.sendForm(id, form, {
channel: privateData.support,
curvePublic: user.curvePublic
});
id = Util.uid();
if (sent) {
$('.cp-sidebarlayout-category[data-category="tickets"]').click();
}
});
return $div;
};
// Support is disabled...
create['disabled'] = function () {
var key = 'disabled';
var $div = makeBlock(key);
return $div;
};
var hideCategories = function () {
APP.$rightside.find('> div').hide();
};
var showCategories = function (cat) {
hideCategories();
cat.forEach(function (c) {
APP.$rightside.find('.'+c).show();
});
};
var createLeftside = function () {
var $categories = $('<div>', {'class': 'cp-sidebarlayout-categories'})
.appendTo(APP.$leftside);
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var active = privateData.category || 'tickets';
common.setHash(active);
Object.keys(categories).forEach(function (key) {
var $category = $('<div>', {
'class': 'cp-sidebarlayout-category',
'data-category': key
}).appendTo($categories);
if (key === 'tickets') { $category.append($('<span>', {'class': 'fa fa-envelope-o'})); }
if (key === 'new') { $category.append($('<span>', {'class': 'fa fa-life-ring'})); }
if (key === active) {
$category.addClass('cp-leftside-active');
}
$category.click(function () {
if (!Array.isArray(categories[key]) && categories[key].onClick) {
categories[key].onClick();
return;
}
active = key;
common.setHash(key);
$categories.find('.cp-leftside-active').removeClass('cp-leftside-active');
$category.addClass('cp-leftside-active');
showCategories(categories[key]);
});
$category.append(Messages['support_cat_'+key] || key);
});
showCategories(categories[active]);
};
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
var configTb = {
displayed: displayed,
sfCommon: common,
$container: APP.$toolbar,
pageTitle: Messages.supportPage,
metadataMgr: common.getMetadataMgr(),
};
APP.toolbar = Toolbar.create(configTb);
APP.toolbar.$rightside.hide();
};
nThen(function (waitFor) {
$(waitFor(UI.addLoadingScreen));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (waitFor) {
APP.$container = $('#cp-sidebarlayout-container');
APP.$toolbar = $('#cp-toolbar');
APP.$leftside = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);
APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container);
var sFrameChan = common.getSframeChannel();
sFrameChan.onReady(waitFor());
}).nThen(function (/*waitFor*/) {
createToolbar();
metadataMgr = common.getMetadataMgr();
privateData = metadataMgr.getPrivateData();
common.setTabTitle(Messages.supportPage);
APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly;
APP.support = Support.create(common, false);
// Content
var $rightside = APP.$rightside;
var addItem = function (cssClass) {
var item = cssClass.slice(11); // remove 'cp-support-'
if (typeof (create[item]) === "function") {
$rightside.append(create[item]());
}
};
for (var cat in categories) {
if (!Array.isArray(categories[cat])) { continue; }
categories[cat].forEach(addItem);
}
createLeftside();
UI.removeLoadingScreen();
});
});

@ -0,0 +1,52 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'/common/dom-ready.js',
'/common/requireconfig.js',
'/common/sframe-common-outer.js'
], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) {
var requireConfig = RequireConfig();
// Loaded in load #2
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
document.getElementById('sbox-iframe').setAttribute('src',
ApiConfig.httpSafeOrigin + '/support/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 category;
if (window.location.hash) {
category = window.location.hash.slice(1);
window.location.hash = '';
}
var addData = function (obj) {
if (category) { obj.category = category; }
};
SFCommonO.start({
noRealtime: true,
addData: addData
});
});
});

@ -0,0 +1,229 @@
define([
'jquery',
'/api/config',
'/common/hyperscript.js',
'/common/common-interface.js',
'/common/common-hash.js',
'/common/common-util.js',
'/customize/messages.js',
], function ($, ApiConfig, h, UI, Hash, Util, Messages) {
var send = function (ctx, id, type, data, dest) {
var common = ctx.common;
var supportKey = ApiConfig.supportMailbox;
var supportChannel = Hash.getChannelIdFromKey(supportKey);
var metadataMgr = common.getMetadataMgr();
var user = metadataMgr.getUserData();
var privateData = metadataMgr.getPrivateData();
data = data || {};
data.sender = {
name: user.name,
channel: privateData.support,
curvePublic: user.curvePublic,
edPublic: privateData.edPublic,
notifications: user.notifications,
};
data.id = id;
data.time = +new Date();
if (!ctx.isAdmin) {
data.sender.userAgent = window.navigator && window.navigator.userAgent;
}
// Send the message to the admin mailbox and to the user mailbox
common.mailbox.sendTo(type, data, {
channel: supportChannel,
curvePublic: supportKey
});
common.mailbox.sendTo(type, data, {
channel: dest.channel,
curvePublic: dest.curvePublic
});
if (ctx.isAdmin) {
common.mailbox.sendTo('SUPPORT_MESSAGE', {}, {
channel: dest.notifications,
curvePublic: dest.curvePublic
});
}
};
var sendForm = function (ctx, id, form, dest) {
var $title = $(form).find('.cp-support-form-title');
var $content = $(form).find('.cp-support-form-msg');
var title = $title.val().trim();
if (!title) {
return void UI.alert(Messages.support_formTitleError);
}
var content = $content.val().trim();
if (!content) {
return void UI.alert(Messages.support_formContentError);
}
$content.val('');
$title.val('');
send(ctx, id, 'TICKET', {
title: title,
message: content,
}, dest);
return true;
};
var makeForm = function (cb, title) {
var button;
if (typeof(cb) === "function") {
button = h('button.btn.btn-primary.cp-support-list-send', Messages.contacts_send);
$(button).click(cb);
}
var cancel = title ? h('button.btn.btn-secondary', Messages.cancel) : undefined;
var content = [
h('hr'),
h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), {
placeholder: Messages.support_formTitle,
type: 'text',
value: title || ''
}),
cb ? undefined : h('br'),
h('textarea.cp-support-form-msg', {
placeholder: Messages.support_formMessage
}),
h('hr'),
button,
cancel
];
var form = h('div.cp-support-form-container', content);
$(cancel).click(function () {
$(form).closest('.cp-support-list-ticket').find('.cp-support-list-actions').show();
$(form).remove();
});
return form;
};
var makeTicket = function (ctx, $div, content, onHide) {
var ticketTitle = content.title + ' (#' + content.id + ')';
var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer);
var close = h('button.btn.btn-danger.cp-support-close', Messages.support_close);
var hide = h('button.btn.btn-danger.cp-support-hide', Messages.support_remove);
var actions = h('div.cp-support-list-actions', [
answer,
close,
hide
]);
var $ticket = $(h('div.cp-support-list-ticket', {
'data-id': content.id
}, [
h('h2', ticketTitle),
actions
]));
$(close).click(function () {
send(ctx, content.id, 'CLOSE', {}, content.sender);
});
$(hide).click(function () {
if (typeof(onHide) !== "function") { return; }
onHide();
});
$(answer).click(function () {
$ticket.find('.cp-support-form-container').remove();
$(actions).hide();
var form = makeForm(function () {
var sent = sendForm(ctx, content.id, form, content.sender);
if (sent) {
$(actions).show();
$(form).remove();
}
}, content.title);
$ticket.append(form);
});
$div.append($ticket);
return $ticket;
};
var makeMessage = function (ctx, content, hash) {
var common = ctx.common;
var isAdmin = ctx.isAdmin;
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
// Check content.sender to see if it comes from us or from an admin
var fromMe = content.sender && content.sender.edPublic === privateData.edPublic;
var userData = h('div.cp-support-showdata', [
Messages.support_showData,
h('pre.cp-support-message-data', JSON.stringify(content.sender, 0, 2))
]);
$(userData).click(function () {
$(userData).find('pre').toggle();
});
return h('div.cp-support-list-message', {
'data-hash': hash
}, [
h('div.cp-support-message-from' + (fromMe ? '.cp-support-fromme' : ''), [
UI.setHTML(h('span'), Messages._getKey('support_from', [content.sender.name])),
h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')
]),
h('pre.cp-support-message-content', content.message),
isAdmin ? userData : undefined,
]);
};
var makeCloseMessage = function (ctx, content, hash) {
var common = ctx.common;
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var fromMe = content.sender && content.sender.edPublic === privateData.edPublic;
return h('div.cp-support-list-message', {
'data-hash': hash
}, [
h('div.cp-support-message-from' + (fromMe ? '.cp-support-fromme' : ''), [
UI.setHTML(h('span'), Messages._getKey('support_from', [content.sender.name])),
h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')
]),
h('pre.cp-support-message-content', Messages.support_closed)
]);
};
var create = function (common, isAdmin) {
var ui = {};
var ctx = {
common: common,
isAdmin: isAdmin
};
ui.sendForm = function (id, form, dest) {
return sendForm(ctx, id, form, dest);
};
ui.makeForm = makeForm;
ui.makeTicket = function ($div, content, onHide) {
return makeTicket(ctx, $div, content, onHide);
};
ui.makeMessage = function (content, hash) {
return makeMessage(ctx, content, hash);
};
ui.makeCloseMessage = function (content, hash) {
return makeCloseMessage(ctx, content, hash);
};
return ui;
};
return {
create: create
};
});

@ -14,7 +14,7 @@ define([
var canvas = new Fabric.Canvas(canvas_node); var canvas = new Fabric.Canvas(canvas_node);
var content = userDoc.content; var content = userDoc.content;
canvas.loadFromJSON(content, function () { canvas.loadFromJSON(content, function () {
module.type = 'svg'; module.ext = '.svg';
cb(canvas.toSVG()); cb(canvas.toSVG());
}); });
}; };

@ -415,11 +415,11 @@ define([
setEditable(!locked); setEditable(!locked);
}); });
framework.setFileExporter('png', function (cb) { framework.setFileExporter('.png', function (cb) {
$canvas[0].toBlob(function (blob) { $canvas[0].toBlob(function (blob) {
cb(blob); cb(blob);
}); });
}); }, true);
framework.setNormalizer(function (c) { framework.setNormalizer(function (c) {
return { return {

Loading…
Cancel
Save