Compare commits
246 Commits
291b6dcce6
...
1401925f5a
Author | SHA1 | Date |
---|---|---|
David Benque | 1401925f5a | |
David Benque | fea4f179d1 | |
David Benque | 3cfccf8024 | |
yflory | 6eab023a5a | |
yflory | 4709b6740d | |
yflory | d5fefb5946 | |
Weblate | 0e1ff02a93 | |
yflory | f01e21eac7 | |
yflory | 8245189bcb | |
Weblate | 1715250677 | |
Weblate | 20a4c8114e | |
Weblate | 7d8fabb63c | |
Weblate | 5b5c11fcfb | |
Weblate | e6d4b75a75 | |
yflory | 770fb3d657 | |
yflory | 74707ea723 | |
ansuz | 2834842f3a | |
creme | 940d7d3118 | |
David Benque | a7463c1987 | |
David Benque | 372e0dd3e6 | |
ansuz | 0231fc684d | |
ansuz | 505b42f740 | |
yflory | 75de90c8b5 | |
Weblate | de240c3af4 | |
Weblate | 519204ceae | |
Weblate | 382e49e373 | |
David Benque | 4c1850d0a6 | |
Weblate | dd6d2fa959 | |
Mathilde Grünig | ece69b8c37 | |
David Benque | 1af5c08bfa | |
Weblate | 9a63c3b2ab | |
David Benque | 6f4d922be2 | |
David Benque | ebf103bb4e | |
ansuz | 4aaa2bd71c | |
Weblate | b3cbf34bc2 | |
David Benque | bf07056bab | |
David Benque | 839bc57849 | |
Weblate | 61589934d7 | |
David Benque | 391628510f | |
David Benque | 8a2f71a573 | |
David Benque | dc91ecd78d | |
David Benque | 4f384c4b89 | |
Weblate | 109724def5 | |
Weblate | 82a5696f41 | |
ansuz | a804a0d9bc | |
Weblate | 30de40cdbc | |
Weblate | aa7dcfdf26 | |
ansuz | f39bcb2806 | |
ansuz | 9a5b54d091 | |
ansuz | 78ba1a7f18 | |
ansuz | b29463c4da | |
dependabot[bot] | 64407f7de6 | |
ansuz | 927a62ad5a | |
ansuz | fbb1f43ede | |
ansuz | 4e1a035ed9 | |
yflory | 3fce59aefc | |
Weblate | 5df44b10cc | |
Weblate | 2d46de5ad5 | |
yflory | 5f6f4edf19 | |
yflory | b6274dbf0c | |
David Benque | 81a7fed01f | |
David Benque | c35cd6a5f6 | |
Weblate | c0abc82983 | |
Weblate | 46db0213dc | |
Weblate | 6ec43e91ca | |
David Benque | 8995255932 | |
David Benque | 4cffb0d1be | |
David Benque | 830281fd68 | |
yflory | 741bd300af | |
yflory | 83744740e5 | |
yflory | 4434cdca17 | |
yflory | 5521db2441 | |
yflory | ab1695900c | |
David Benque | cc852cc505 | |
yflory | 1eaa09df3f | |
yflory | f1ff39fa4f | |
David Benque | 3d9ec26fcb | |
David Benque | 7563fc3d92 | |
dependabot[bot] | 0181e28979 | |
Weblate | 034e4b53b5 | |
Weblate | b6a272a1df | |
Weblate | 97390d0da4 | |
Weblate | cd622d1c62 | |
David Benque | f535c778e3 | |
David Benque | 38c3515fbb | |
yflory | 55f3b910ee | |
yflory | 97fde60e84 | |
yflory | 600771682a | |
yflory | f0bc1ef07a | |
yflory | 6e555c73b4 | |
yflory | ad0c2e90dc | |
yflory | c074eab7b7 | |
yflory | 99011f305a | |
yflory | b2887a5d69 | |
Mathilde Grünig | 37ccaddbbe | |
David Benque | 16bced389a | |
Mathilde Grünig | 1b731e2643 | |
David Benque | 3d0d4f342d | |
dependabot[bot] | ec753b0be0 | |
yflory | 27e6f9a34b | |
David Benque | 0184030f18 | |
yflory | 9ef6ea4dff | |
yflory | d03d06e3c2 | |
yflory | 112d3a04bd | |
yflory | 59c13c506c | |
yflory | 119efa1180 | |
Weblate | 92aeae1219 | |
Weblate | e3b73d4470 | |
Weblate | dff0b2e4c3 | |
Weblate | 1b66f210c8 | |
yflory | 15d9a9c703 | |
yflory | fb079c49bf | |
yflory | acd7d9654d | |
Weblate | 90611b03a0 | |
Weblate | 1204d9df4b | |
yflory | 4f23dc53c3 | |
yflory | d896b0dfd6 | |
yflory | f27431a2f8 | |
yflory | 01a2bb7765 | |
yflory | b8b485c99d | |
yflory | 5d350f1c45 | |
yflory | e9540b9a00 | |
yflory | 7eeb6bb165 | |
yflory | 1d062c98f2 | |
yflory | 8d250722fd | |
yflory | 51c50db166 | |
yflory | 614d3834a3 | |
yflory | 9c34e9e779 | |
yflory | f0a3e0eb81 | |
David Benque | 89d0a7ef28 | |
yflory | 846b91907b | |
yflory | c49949f810 | |
David Benque | 0411adc0b7 | |
David Benque | f8b06af65b | |
Weblate | cb26072115 | |
Weblate | 39dc8dbaf6 | |
Weblate | 2ef2cc3517 | |
David Benque | 795ec4b22f | |
David Benque | d37e3c61e7 | |
David Benque | b2022f5e73 | |
Weblate | e7be485e89 | |
Weblate | 5d29c0134d | |
Weblate | e7c0a0c1f6 | |
Weblate | baea5166b0 | |
yflory | b605c516f0 | |
yflory | 89448115c5 | |
David Benque | 736d4a531a | |
David Benque | 87e46dd4ae | |
David Benque | 1e99283db8 | |
David Benque | fbb991bfad | |
yflory | b85801e830 | |
yflory | 0e27daffc0 | |
Weblate | 2a67360ee1 | |
Weblate | 3549879e73 | |
yflory | bfd1b5138a | |
yflory | d20b3ebee3 | |
yflory | 1838848ec8 | |
yflory | 57d6f1e683 | |
yflory | 0faa99fbfc | |
yflory | 8e05f159ae | |
David Benque | bd82e86228 | |
David Benque | 4f15fda226 | |
yflory | bde6bb0032 | |
ansuz | bdf8762fb6 | |
David Benque | 1ab0208f5b | |
David Benque | 81e00842b9 | |
David Benque | 90a7b89a0c | |
yflory | 8a3be878e8 | |
yflory | c3df1bb0ec | |
ansuz | 2e4655e29c | |
David Benque | 0b87f1f9c6 | |
David Benque | 5d96f94766 | |
yflory | ce5609ea6a | |
yflory | f3dc8d059e | |
yflory | 16cdf2ccdc | |
yflory | c11b83dd5a | |
David Benque | 99da655225 | |
David Benque | 707ac44536 | |
David Benque | 176f6ab594 | |
David Benque | e568027d14 | |
David Baker | bf69a576bb | |
yflory | 6a1c64fe9a | |
ansuz | e59b1fc933 | |
ansuz | 1acdb4180d | |
ansuz | c03feef96e | |
ansuz | 863ab4f380 | |
ansuz | 3e3fc4f9e4 | |
ansuz | 559e2d1e57 | |
ansuz | 01cdfa1bbc | |
ansuz | 5b4b68b31a | |
David Benque | acfbdccf6f | |
Mathilde | f1490a2835 | |
David Benque | 699922fae6 | |
Mathilde Blanchemanche | 4eccf22c42 | |
Mathilde Blanchemanche | c2b540793d | |
Mathilde Blanchemanche | 51d4a96ef8 | |
Mathilde Blanchemanche | 34313ea048 | |
Mathilde Blanchemanche | 8d105d6f16 | |
Mathilde Blanchemanche | ef495142b0 | |
ansuz | fede73efb1 | |
ansuz | 9098af75ab | |
ansuz | a8dcdbcbcb | |
ansuz | 112ad47bc6 | |
ansuz | 961e816cef | |
ansuz | 174d97c442 | |
yflory | 89aabacc55 | |
yflory | b7024b23f5 | |
David Benque | 5c51fb3d84 | |
David Benque | 29e5bf63dd | |
David Benque | 34b5f66047 | |
David Benque | 31ca0c4518 | |
David Benque | 7aaf1afeff | |
yflory | 57c3c28cb0 | |
yflory | c3b501b431 | |
yflory | 175fb7e1dc | |
yflory | 6ddfd09805 | |
yflory | 65b00736bc | |
yflory | af923170a4 | |
yflory | 2e983bf52a | |
yflory | 0ee48d40f2 | |
David Benque | 57e801da98 | |
yflory | f27d779aef | |
yflory | ae32732483 | |
yflory | 2dbefe864a | |
yflory | 0c273b1b3d | |
yflory | d35b42d207 | |
yflory | 3fa92fa155 | |
yflory | 99df9bc21a | |
yflory | ee5e1f8335 | |
yflory | c123434fa0 | |
yflory | 6ca6ecd283 | |
yflory | 1d001f4ca4 | |
Maxime Cesson | 44e5d021ba | |
Maxime Cesson | 13cba52778 | |
Maxime Cesson | 7906079a2b | |
yflory | a5c4bc98ba | |
Maxime Cesson | c4410f52d7 | |
yflory | bdddb231af | |
Maxime Cesson | febe51aabc | |
Maxime Cesson | 2c3f1f3b07 | |
Maxime Cesson | e1c02d784e | |
Maxime Cesson | 581cb917b7 | |
Maxime Cesson | c2b3ed7ae7 | |
Maxime Cesson | e382833545 | |
Maxime Cesson | caa4666e4a | |
ansuz | a93ab05310 |
|
@ -1,5 +1,5 @@
|
|||
name: Bug report
|
||||
description: Report a reproducible bug for CryptPad. (NOT to be used for support questions.)
|
||||
name: Reproducible bug report
|
||||
description: Report a reproducible bug ONLY, otherwise see the links below
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: CryptPad.fr issue
|
||||
- name: Bug report
|
||||
url: https://forum.cryptpad.org/t/bug-report
|
||||
about: Use the appropriate tags & describe your issue in much details as possible
|
||||
- name: Feature request
|
||||
url: https://forum.cryptpad.org/t/feature-request
|
||||
about: Check if an existing topic doesn't already cover the feature you'd like
|
||||
- name: Generic question
|
||||
url: https://forum.cryptpad.org/t/general
|
||||
about: Feel free to use the forum to ask any question you might have
|
||||
- name: CryptPad.fr flagship instance issue
|
||||
url: https://cryptpad.fr/support/#new
|
||||
about: Please use the integrated support form for CryptPad.fr
|
||||
about: Issue with an account or document on cryptpad.fr only
|
||||
- name: Report a security vulnerability
|
||||
url: https://ouvaton.link/pOgHev
|
||||
about: Please give us appropriate time to verify, respond and fix before disclosure
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
name: Feature Request
|
||||
description: Suggest an idea for CryptPad.
|
||||
labels: ["Feature Request"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Contribution guidelines
|
||||
description: Please read the code of conduct before proceeding.
|
||||
options:
|
||||
- label: I've read the [code of conduct](https://github.com/xwiki-labs/cryptpad/blob/main/CODE_OF_CONDUCT.md) and wholeheartedly agree
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: I would like to request a new feature for CryptPad
|
||||
description: Prior to creating a new issue, please check following:** *(fill out each checkbox with an `X` once done)*
|
||||
options:
|
||||
- label: I have searched past issues and labels to check that my question/idea does not exist
|
||||
required: true
|
||||
- label: I have understood that this report is dedicated for feature requests, and not for support-related inquiries.
|
||||
required: true
|
||||
- label: I have understood that answers are voluntary and community-driven, and not commercial support.
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Please describe your idea in a reasonable amount of detail.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: Please describe how your idea would benefit you and other users.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request.
|
|
@ -8,7 +8,6 @@ www/common/onlyoffice/v2*
|
|||
www/common/onlyoffice/v4
|
||||
www/common/onlyoffice/v5
|
||||
|
||||
server.js
|
||||
www/scratch
|
||||
www/accounts
|
||||
www/lib
|
||||
|
|
93
CHANGELOG.md
93
CHANGELOG.md
|
@ -1,3 +1,96 @@
|
|||
# 5.2.1
|
||||
|
||||
## Goals
|
||||
|
||||
This minor releases fixes a bug with one of the Form features introduced in 5.2.0.
|
||||
|
||||
We took the opportunity to include two other fixes for older issues.
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- The option to delete all responses to a form was not available to form authors when the form had been created in a drive (user or team) using the **+ NEW** button
|
||||
|
||||
- Drag & drop from a shared folder into the Templates folder made documents "disappear". They would reappear in the root of the drive when using a new worker (after all CryptPad tabs had been closed)
|
||||
|
||||
- Clicking a link in a Calendar event location field failed to open
|
||||
|
||||
## Update notes
|
||||
|
||||
Our `5.2.0` release introduced some changes to the Nginx configuration. If you are not already running `5.2.0` we recommend following the upgrade notes for that version first, and then updating to `5.2.1`
|
||||
|
||||
To do so:
|
||||
|
||||
1. Stop your server
|
||||
2. Get the latest code with git
|
||||
|
||||
```bash
|
||||
git fetch origin --tags
|
||||
git checkout 5.2.1
|
||||
```
|
||||
|
||||
1. Install the latest dependencies with `bower update`
|
||||
2. Restart your server
|
||||
3. Review your instance's checkup page to ensure that you are passing all tests
|
||||
|
||||
# 5.2.0
|
||||
|
||||
## Goals
|
||||
|
||||
This release is focused on addressing long-standing user feedback with new features. The most requested are improvements to Forms—multiple submissions and the ability to delete responses—as well as recurring events in Calendar.
|
||||
|
||||
## Features
|
||||
|
||||
- Forms
|
||||
- New setting to allow participants (including Guests) to submit a form multiple times and/or delete their responses
|
||||
- Notifications for form owners when new responses are submitted
|
||||
- New option for form authors to delete all responses
|
||||
- New option for form authors/auditors to export responses as JSON (in addition to existing CSV and CryptPad Sheet)
|
||||
- Settings have been refactored in a modal with a summary in the main editor view
|
||||
- Display fixes for long questions/options in some question types
|
||||
|
||||
- Calendar
|
||||
- New event settings to repeat periodically
|
||||
- quick default patterns (e.g. weekly on Mondays, yearly on December 14th, etc), and custom intervals
|
||||
- modify one, future, or all events
|
||||
- easily stop repetition from event preview
|
||||
|
||||
- Drive
|
||||
- New button to filter the drive view by document type
|
||||
|
||||
- Teams
|
||||
- Improved onboarding with the ability to use the same invitation link for a set number of people. Previously each link was limited to one use
|
||||
- Initial role can now be set for invitation links, the recipient is assigned the role directly when joining, previously all new members joined as "Viewers"
|
||||
|
||||
- Code
|
||||
- Asciidoc syntax support AND asciidoc rendering
|
||||
- New jade language support
|
||||
- Removed duplicate C-language option
|
||||
|
||||
- /checkup/
|
||||
- [new test to confirm that public instances are open for registration](https://github.com/xwiki-labs/cryptpad/commit/174d97c442d5400d512dfccc478fd9fbd6fa075c)
|
||||
- new test to check that the host provides an HSTS header
|
||||
|
||||
## Update notes
|
||||
|
||||
To update from `5.1.0` to `5.2.0`:
|
||||
|
||||
1. Read the **Nginx** section below to ensure you are using the right version and update your reverse proxy configuration to match the settings in our current `./docs/example.nginx.conf`
|
||||
2. Reload nginx
|
||||
3. Stop your API server
|
||||
4. Fetch the latest code with git
|
||||
5. Install the latest dependencies with `bower update` and `npm i`
|
||||
6. Restart your server
|
||||
7. Review your instance's checkup page to ensure that all tests are passing
|
||||
|
||||
### Nginx
|
||||
|
||||
We added some directives that may cause issues with older versions of Nginx. We now recommend and only support [Nginx stable](https://nginx.org/en/download.html). Please note that if you are running below `v1.14.2`, applying this update will likely result in breakage.
|
||||
- Internet Protocol version 6 ([IPv6](https://en.wikipedia.org/wiki/IPv6)) support
|
||||
- TLS generation, see [the recent tutorial](https://blog.cryptpad.org/2022/12/12/tutorial-nginx-tls-acme/) on our blog
|
||||
- Better [TLS sessions](https://vincent.bernat.ch/en/blog/2011-ssl-session-reuse-rfc5077), handling timeout, tickets & longer cache
|
||||
- Longer [HTTP Strict Transport Security](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) (HSTS), now 2 years
|
||||
- [Online Certificate Status Protocol](https://en.wikipedia.org/wiki/OCSP_stapling) (OCSP) stapling support
|
||||
|
||||
# 5.1.0
|
||||
|
||||
## Goals
|
||||
|
|
|
@ -95,7 +95,7 @@ define([
|
|||
return h('a', attrs, [icon, text]);
|
||||
};
|
||||
|
||||
Pages.versionString = "5.1.0";
|
||||
Pages.versionString = "5.2.1";
|
||||
|
||||
var customURLs = Pages.customURLs = {};
|
||||
(function () {
|
||||
|
|
|
@ -183,6 +183,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.cp-dropdown-content {
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-alertify-type-container {
|
||||
overflow: visible !important;
|
||||
|
|
|
@ -11,10 +11,12 @@
|
|||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: @cp_scrollbar-fg;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
|
@ -120,6 +120,7 @@
|
|||
border-width: 0 @checkmark-width @checkmark-width 0;
|
||||
border-width: 0 var(--checkmark-width) var(--checkmark-width) 0;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 5px @cp_checkmark-back1;
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
button {
|
||||
.fa-caret-down {
|
||||
margin-right: 1em !important;
|
||||
margin-right: 0.5em !important;
|
||||
}
|
||||
* {
|
||||
.tools_unselectable();
|
||||
|
|
|
@ -91,7 +91,10 @@
|
|||
height: 100%;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
background-color: @cp_buttons-default-color;
|
||||
&.btn-primary, &.btn-secondary {
|
||||
background-color: @cp_buttons-primary;
|
||||
}
|
||||
&.danger, &.btn-danger, &.danger-alt, &.btn-danger-alt {
|
||||
background-color: @cp_buttons-red;
|
||||
}
|
||||
|
@ -327,6 +330,9 @@
|
|||
fill: @cryptpad_text_col;
|
||||
}
|
||||
}
|
||||
.flatpickr-monthDropdown-month {
|
||||
background: @cp_flatpickr-bg;
|
||||
}
|
||||
}
|
||||
.flatpickr-current-month {
|
||||
span.cur-month:hover {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
& {
|
||||
|
||||
each(@colortheme_apps, {
|
||||
button .cp-icon-color-@{key},
|
||||
.cp-icon-color-@{key} { color: @value; }
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,14 @@
|
|||
margin-bottom: 10px !important;
|
||||
}
|
||||
}
|
||||
// todo ul, ol
|
||||
|
||||
// fix silly spacing around sublists in "loose lists"
|
||||
:is(ul, ol) li > p {
|
||||
& + :is(ul, ol) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// TOC
|
||||
div.cp-md-toc {
|
||||
background: @cp_markdown-bg;
|
||||
|
|
|
@ -123,12 +123,20 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
&.cp-teams-invite-role {
|
||||
.cp-radio:not(:first-child) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.cp-teams-help {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
.cp-teams-invite-message {
|
||||
resize: none;
|
||||
}
|
||||
.cp-teams-invite-alert {
|
||||
margin-top: 10px;
|
||||
margin: 10px 0px 0px 0px !important;
|
||||
}
|
||||
.cp-teams-invite-spinner {
|
||||
font-size: 1.2em;
|
||||
|
|
|
@ -27,14 +27,16 @@
|
|||
color: @cryptpad_color_red;
|
||||
}
|
||||
}
|
||||
.cp-avatar {
|
||||
.avatar_main(30px);
|
||||
padding: 0 5px;
|
||||
.cp-reminder, .cp-avatar {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: @cp_dropdown-bg-hover;
|
||||
}
|
||||
}
|
||||
.cp-avatar {
|
||||
.avatar_main(30px);
|
||||
padding: 0 5px;
|
||||
}
|
||||
.cp-notification-content {
|
||||
flex: 1;
|
||||
align-items: stretch;
|
||||
|
|
|
@ -967,6 +967,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.cp-toolbar-dropdown-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cp-toolbar-bottom {
|
||||
color: @cp_toolbar-bottom-fg;
|
||||
display: inline-flex;
|
||||
|
@ -998,11 +1001,15 @@
|
|||
.fa, .cptools {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.cp-dropdown-button-title .cp-icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: fade(@cp_toolbar-bottom-bg, 70%);
|
||||
}
|
||||
}
|
||||
.cp-toolbar-bottom-left > button,
|
||||
.cp-toolbar-bottom-left > span > button,
|
||||
.cp-toolbar-bottom-mid > button,
|
||||
.cp-toolbar-bottom-right > button,
|
||||
.cp-toolbar-bottom-right > span > button {
|
||||
|
@ -1070,7 +1077,7 @@
|
|||
.cp-toolbar-name, .cp-button-name {
|
||||
display: none;
|
||||
}
|
||||
i {
|
||||
i, span {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
# Let's Encrypt webroot
|
||||
include letsencrypt-webroot;
|
||||
|
||||
# CryptPad serves static assets over these two domains.
|
||||
# `main_domain` is what users will enter in their address bar.
|
||||
|
@ -46,26 +50,35 @@ server {
|
|||
# IMPORTANT: this config is intended to serve assets for at least two domains
|
||||
# (your main domain and your sandbox domain). As such, you'll need to generate a single SSL certificate
|
||||
# that includes both domains in order for things to work as expected.
|
||||
ssl_certificate /home/cryptpad/.acme.sh/your-main-domain.com/fullchain.cer;
|
||||
ssl_certificate_key /home/cryptpad/.acme.sh/your-main-domain.com/your-main-domain.com.key;
|
||||
ssl_trusted_certificate /home/cryptpad/.acme.sh/your-main-domain.com/ca.cer;
|
||||
ssl_certificate /etc/ssl/lets-encrypt/your-main-domain.com/cert;
|
||||
ssl_certificate_key /etc/ssl/lets-encrypt/your-main-domain.com/key;
|
||||
|
||||
# diffie-hellman parameters are used to negotiate keys for your session
|
||||
# generate strong parameters using the following command
|
||||
ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -out /etc/nginx/dhparam.pem 4096
|
||||
|
||||
# Speeds things up a little bit when resuming a session
|
||||
ssl_session_timeout 5m;
|
||||
ssl_session_cache shared:SSL:5m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# You'll need nginx 1.13.0 or better to support TLSv1.3
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# https://cipherli.st/
|
||||
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
|
||||
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
|
||||
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
||||
|
||||
# OCSP stapling
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# verify chain of trust of OCSP response using Root CA and Intermediate certs
|
||||
ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
|
||||
|
||||
# replace with the IP address of your resolver
|
||||
resolver 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 9.9.9.9 149.112.112.112 208.67.222.222 208.67.220.220;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Access-Control-Allow-Origin "${allowed_origins}";
|
||||
|
@ -84,12 +97,17 @@ server {
|
|||
error_page 404 /customize.dist/404.html;
|
||||
|
||||
# any static assets loaded with "ver=" in their URL will be cached for a year
|
||||
if ($uri ~ ^(\/|.*\/|.*\.html)$) {
|
||||
set $cacheControl no-cache;
|
||||
}
|
||||
if ($args ~ ver=) {
|
||||
set $cacheControl max-age=31536000;
|
||||
}
|
||||
# This rule overrides the above caching directive and makes things somewhat less efficient.
|
||||
# We had inverted them as an optimization, but Safari 16 introduced a bug that interpreted
|
||||
# some important headers incorrectly when loading these files from cache.
|
||||
# This is why we can't have nice things :(
|
||||
if ($uri ~ ^(\/|.*\/|.*\.html)$) {
|
||||
set $cacheControl no-cache;
|
||||
}
|
||||
|
||||
# Will not set any header if it is emptystring
|
||||
add_header Cache-Control $cacheControl;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ const nThen = require("nthen");
|
|||
const Core = require("./core");
|
||||
const Metadata = require("./metadata");
|
||||
const HK = require("../hk-util");
|
||||
const Nacl = require("tweetnacl/nacl-fast");
|
||||
|
||||
Channel.disconnectChannelMembers = function (Env, Server, channelId, code, cb) {
|
||||
var done = Util.once(Util.mkAsync(cb));
|
||||
|
@ -207,6 +208,40 @@ Channel.trimHistory = function (Env, safeKey, data, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
// Delete a signed mailbox message. This is used when users want
|
||||
// to delete their form reponses.
|
||||
Channel.deleteMailboxMessage = function (Env, data, cb) {
|
||||
const channelId = data.channel;
|
||||
const hash = data.hash;
|
||||
const proof = data.proof;
|
||||
let nonce, proofBytes;
|
||||
try {
|
||||
nonce = Nacl.util.decodeBase64(proof.split('|')[0]);
|
||||
proofBytes = Nacl.util.decodeBase64(proof.split('|')[1]);
|
||||
} catch (e) {
|
||||
return void cb('EINVAL');
|
||||
}
|
||||
Env.msgStore.deleteChannelLine(channelId, hash, function (msg) {
|
||||
// Check if you're allowed to delete this hash
|
||||
try {
|
||||
const mySecret = new Uint8Array(32);
|
||||
const msgBytes = Nacl.util.decodeBase64(msg).subarray(64); // Remove signature
|
||||
const theirPublic = msgBytes.subarray(24,56); // 0-24 = nonce; 24-56=publickey (32 bytes)
|
||||
const hashBytes = Nacl.box.open(proofBytes, nonce, theirPublic, mySecret);
|
||||
return Nacl.util.encodeUTF8(hashBytes) === hash;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}, function (err) {
|
||||
if (err) { return void cb(err); }
|
||||
// clear historyKeeper's cache for this channel
|
||||
Env.historyKeeper.channelClose(channelId);
|
||||
cb();
|
||||
delete Env.channel_cache[channelId];
|
||||
delete Env.metadata_cache[channelId];
|
||||
});
|
||||
};
|
||||
|
||||
var ARRAY_LINE = /^\[/;
|
||||
|
||||
/* Files can contain metadata but not content
|
||||
|
@ -320,10 +355,11 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
|
|||
Server.getChannelUserList(channelId).forEach(function (userId) {
|
||||
Server.send(userId, fullMessage);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
cb();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ const Core = require("./commands/core");
|
|||
const Quota = require("./commands/quota");
|
||||
const Util = require("./common-util");
|
||||
const Package = require("../package.json");
|
||||
const Path = require("path");
|
||||
|
||||
var canonicalizeOrigin = function (s) {
|
||||
if (typeof(s) === 'undefined') { return; }
|
||||
|
@ -296,7 +297,7 @@ module.exports.create = function (config) {
|
|||
var paths = Env.paths;
|
||||
|
||||
var keyOrDefaultString = function (key, def) {
|
||||
return typeof(config[key]) === 'string'? config[key]: def;
|
||||
return Path.resolve(typeof(config[key]) === 'string'? config[key]: def);
|
||||
};
|
||||
|
||||
Env.incrementBytesWritten = function (n) {
|
||||
|
|
|
@ -31,7 +31,8 @@ the owners field is guaranteed to exist.
|
|||
* mailbox <STRING|MAP>
|
||||
* ADD_MAILBOX
|
||||
* RM_MAILBOX
|
||||
|
||||
* deleteLines <BOOLEAN>
|
||||
* ALLOW_LINE_DELETION
|
||||
*/
|
||||
|
||||
var commands = {};
|
||||
|
@ -71,6 +72,24 @@ commands.RESTRICT_ACCESS = function (meta, args) {
|
|||
return true;
|
||||
};
|
||||
|
||||
// ["ALLOW_LINE_DELETION", [true], 1561623438989]
|
||||
// ["ALLOW_LINE_DELETION", [false], 1561623438989]
|
||||
commands.ALLOW_LINE_DELETION = function (meta, args) {
|
||||
if (!Array.isArray(args) || typeof(args[0]) !== 'boolean') {
|
||||
throw new Error('INVALID_STATE');
|
||||
}
|
||||
|
||||
var bool = args[0];
|
||||
|
||||
// reject the proposed command if there is no change in state
|
||||
if (meta.deleteLines === bool) { return false; }
|
||||
|
||||
// apply the new state
|
||||
meta.deleteLines = args[0];
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// ["ADD_ALLOWED", ["7eEqelGso3EBr5jHlei6av4r9w2B9XZiGGwA1EgZ-5I=", ...], 1561623438989]
|
||||
commands.ADD_ALLOWED = function (meta, args) {
|
||||
if (!Array.isArray(args)) {
|
||||
|
@ -386,6 +405,7 @@ Meta.createLineHandler = function (ref, errorHandler) {
|
|||
ref.meta = {};
|
||||
ref.index = 0;
|
||||
ref.logged = {};
|
||||
var overwritten = false;
|
||||
|
||||
return function (err, line) {
|
||||
if (err) {
|
||||
|
@ -430,6 +450,8 @@ Meta.createLineHandler = function (ref, errorHandler) {
|
|||
// Thus, accept both the first and second lines you process as valid initial state
|
||||
// preferring the second if it exists
|
||||
if (index < 2 && line && typeof(line) === 'object') {
|
||||
if (overwritten) { return; } // hack to avoid overwriting metadata a second time
|
||||
overwritten = true;
|
||||
// special case!
|
||||
ref.meta = line;
|
||||
return;
|
||||
|
|
|
@ -20,6 +20,7 @@ const UNAUTHENTICATED_CALLS = {
|
|||
IS_CHANNEL_PINNED: Pinning.isChannelPinned, // FIXME drop this RPC
|
||||
IS_NEW_CHANNEL: Channel.isNewChannel,
|
||||
WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage,
|
||||
DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage,
|
||||
GET_METADATA: Metadata.getMetadata,
|
||||
};
|
||||
|
||||
|
|
|
@ -73,9 +73,11 @@ Stats.instanceData = function (Env) {
|
|||
//data.archiveRetentionTime = Env.archiveRetentionTime,
|
||||
}
|
||||
|
||||
// we won't consider instances for public listings
|
||||
// unless they opt to provide more info about themselves
|
||||
if (!Env.provideAggregateStatistics) { return data; }
|
||||
// Admins can opt-in to providing more detailed information about the extent of the instance's usage
|
||||
if (!Env.provideAggregateStatistics) {
|
||||
// check how many instances provide stats before we put more work into it
|
||||
data.providesAggregateStatistics = true;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
|
|
@ -928,7 +928,7 @@ var getMessages = function (env, chanName, handler, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
var trimChannel = function (env, channelName, hash, _cb) {
|
||||
var filterMessages = function (env, channelName, check, filterHandler, _cb) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
// this function is queued as a blocking action for the relevant channel
|
||||
|
||||
|
@ -985,6 +985,13 @@ var trimChannel = function (env, channelName, hash, _cb) {
|
|||
}
|
||||
// if there were no errors just fall through to the next block
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
// If we want to delete a signle line of the file, make sure this pad allows it
|
||||
if (typeof(check) !== "function") { return; }
|
||||
if (!metadataReference.meta || !check(metadataReference.meta)) {
|
||||
w.abort();
|
||||
return void cb("EFORBIDDEN");
|
||||
}
|
||||
}).nThen(function (w) {
|
||||
// create temp buffer writeStream
|
||||
tempStream = Fs.createWriteStream(tempChannelPath, {
|
||||
|
@ -1023,14 +1030,15 @@ var trimChannel = function (env, channelName, hash, _cb) {
|
|||
if (!msg) { return void readMore(); }
|
||||
var msgHash = Extras.getHash(msg[4]);
|
||||
|
||||
if (msgHash === hash) {
|
||||
// everything from this point on should be retained
|
||||
retain = true;
|
||||
return void tempStream.write(s_msg + '\n', function () {
|
||||
var remove = function () { readMore(); };
|
||||
var preserve = function () {
|
||||
tempStream.write(s_msg + '\n', function () {
|
||||
readMore();
|
||||
});
|
||||
}
|
||||
readMore();
|
||||
};
|
||||
var preserveRemaining = function () { retain = true; };
|
||||
|
||||
filterHandler(msg, msgHash, abort, remove, preserve, preserveRemaining);
|
||||
};
|
||||
|
||||
readMessagesBin(env, channelName, 0, handler, w(function (err) {
|
||||
|
@ -1102,13 +1110,42 @@ var trimChannel = function (env, channelName, hash, _cb) {
|
|||
});
|
||||
});
|
||||
};
|
||||
var deleteChannelLine = function (env, channelName, hash, checkRights, _cb) {
|
||||
var check = function (meta) { return Boolean(meta.deleteLines); };
|
||||
var handler = function (msg, msgHash, abort, remove, preserve, preserveRemaining) {
|
||||
if (msgHash === hash) {
|
||||
if (typeof(checkRights) === "function" && !checkRights(msg[4])) {
|
||||
// Not allowed: abort
|
||||
return void abort();
|
||||
}
|
||||
// Line found: remove it and preserve all remaining lines
|
||||
preserveRemaining();
|
||||
return void remove();
|
||||
}
|
||||
// Continue until we find the correct hash
|
||||
preserve();
|
||||
};
|
||||
filterMessages(env, channelName, check, handler, _cb);
|
||||
};
|
||||
var trimChannel = function (env, channelName, hash, _cb) {
|
||||
var handler = function (msg, msgHash, abort, remove, preserve, preserveRemaining) {
|
||||
if (msgHash === hash) {
|
||||
// Everything from this point on should be retained
|
||||
preserveRemaining();
|
||||
return void preserve();
|
||||
}
|
||||
// Remove until we find our hash
|
||||
remove();
|
||||
};
|
||||
filterMessages(env, channelName, null, handler, _cb);
|
||||
};
|
||||
|
||||
module.exports.create = function (conf, _cb) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
|
||||
var env = {
|
||||
root: conf.filePath || './datastore',
|
||||
archiveRoot: conf.archivePath || './data/archive',
|
||||
root: Path.resolve(conf.filePath || './datastore'),
|
||||
archiveRoot: Path.resolve(conf.archivePath || './data/archive'),
|
||||
// supply a volumeId if you want a store to archive channels to and from
|
||||
// to its own subpath within the archive directory
|
||||
volumeId: conf.volumeId || 'datastore',
|
||||
|
@ -1236,6 +1273,12 @@ module.exports.create = function (conf, _cb) {
|
|||
trimChannel(env, channelName, hash, Util.both(cb, next));
|
||||
});
|
||||
},
|
||||
deleteChannelLine: function (channelName, hash, checkRights, cb) {
|
||||
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
||||
schedule.blocking(channelName, function (next) {
|
||||
deleteChannelLine(env, channelName, hash, checkRights, Util.both(cb, next));
|
||||
});
|
||||
},
|
||||
|
||||
// check if a channel exists in the database
|
||||
isChannelAvailable: function (channelName, cb) {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "cryptpad",
|
||||
"description": "realtime collaborative visual editor with zero knowlege server",
|
||||
"version": "5.1.0",
|
||||
"version": "5.2.1",
|
||||
"license": "AGPL-3.0+",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -15,7 +15,7 @@
|
|||
"@mcrowe/minibloom": "^0.2.0",
|
||||
"chainpad-crypto": "^0.2.5",
|
||||
"chainpad-server": "^5.1.0",
|
||||
"express": "~4.16.0",
|
||||
"express": "~4.18.2",
|
||||
"fs-extra": "^7.0.0",
|
||||
"get-folder-size": "^2.0.1",
|
||||
"netflux-websocket": "^0.1.20",
|
||||
|
|
14
readme.md
14
readme.md
|
@ -2,7 +2,9 @@
|
|||
|
||||
CryptPad is a collaboration suite that is end-to-end-encrypted and open-source. It is built to enable collaboration, synchronizing changes to documents in real time. Because all data is encrypted, the service and its administrators have no way of seeing the content being edited and stored.
|
||||
|
||||
![CryptPad screenshot](screenshot.png "Private real-time collaboration on a Rich Text document.")
|
||||
![Drive screenshot](screenshot.png "preview of the CryptDrive")
|
||||
|
||||
![Suite screenshots](screenshot-suite.png "all CyptPad applications: Document, Sheet, Presentation, Form, Kanban, Code, Rich Text, Whiteboard")
|
||||
|
||||
# Installation
|
||||
|
||||
|
@ -62,10 +64,12 @@ More information about this can be found in [our translation guide](/customize.d
|
|||
|
||||
# Contacting Us
|
||||
|
||||
You can reach members of the CryptPad development team on [Twitter](https://twitter.com/cryptpad),
|
||||
via our [GitHub issue tracker](https://github.com/xwiki-labs/cryptpad/issues/), on our
|
||||
[Matrix channel](https://riot.im/app/#/room/#cryptpad:matrix.org), or by
|
||||
[e-mail](mailto:research@xwiki.com).
|
||||
The best places to reach the development team and the community are the [CryptPad Forum](https://forum.cryptpad.org) and the [Matrix chat](https://matrix.to/#/#cryptpad:matrix.xwiki.com)
|
||||
|
||||
The team is also on social media:
|
||||
- Mastodon: [@cryptpad@fosstodon.org](https://fosstodon.org/@cryptpad)
|
||||
- Twitter: [@cryptpad](https://twitter.com/cryptpad)
|
||||
|
||||
|
||||
# Team
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 207 KiB |
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 125 KiB |
73
server.js
73
server.js
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
globals require console
|
||||
globals process
|
||||
*/
|
||||
var Express = require('express');
|
||||
var Http = require('http');
|
||||
|
@ -8,7 +8,6 @@ var Path = require("path");
|
|||
var nThen = require("nthen");
|
||||
var Util = require("./lib/common-util");
|
||||
var Default = require("./lib/defaults");
|
||||
var Keys = require("./lib/keys");
|
||||
|
||||
var config = require("./lib/load-config");
|
||||
var Env = require("./lib/env").create(config);
|
||||
|
@ -116,16 +115,17 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) {
|
|||
});
|
||||
}());
|
||||
|
||||
app.use('/blob', function (req, res, next) {
|
||||
if (req.method === 'HEAD') {
|
||||
Express.static(Path.join(__dirname, Env.paths.blob), {
|
||||
setHeaders: function (res, path, stat) {
|
||||
const serveStatic = Express.static(Env.paths.blob, {
|
||||
setHeaders: function (res) {
|
||||
res.set('Access-Control-Allow-Origin', Env.enableEmbedding? '*': Env.permittedEmbedders);
|
||||
res.set('Access-Control-Allow-Headers', 'Content-Length');
|
||||
res.set('Access-Control-Expose-Headers', 'Content-Length');
|
||||
}
|
||||
})(req, res, next);
|
||||
return;
|
||||
});
|
||||
|
||||
app.use('/blob', function (req, res, next) {
|
||||
if (req.method === 'HEAD') {
|
||||
return void serveStatic(req, res, next);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
@ -150,32 +150,33 @@ app.use(function (req, res, next) {
|
|||
|
||||
// serve custom app content from the customize directory
|
||||
// useful for testing pages customized with opengraph data
|
||||
app.use(Express.static(__dirname + '/customize/www'));
|
||||
app.use(Express.static(__dirname + '/www'));
|
||||
app.use(Express.static(Path.resolve('customize/www')));
|
||||
app.use(Express.static(Path.resolve('www')));
|
||||
|
||||
// FIXME I think this is a regression caused by a recent PR
|
||||
// correct this hack without breaking the contributor's intended behaviour.
|
||||
|
||||
var mainPages = config.mainPages || Default.mainPages();
|
||||
var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$');
|
||||
app.get(mainPagePattern, Express.static(__dirname + '/customize'));
|
||||
app.get(mainPagePattern, Express.static(__dirname + '/customize.dist'));
|
||||
app.get(mainPagePattern, Express.static(Path.resolve('customize')));
|
||||
app.get(mainPagePattern, Express.static(Path.resolve('customize.dist')));
|
||||
|
||||
app.use("/blob", Express.static(Path.join(__dirname, Env.paths.blob), {
|
||||
app.use("/blob", Express.static(Env.paths.blob, {
|
||||
maxAge: Env.DEV_MODE? "0d": "365d"
|
||||
}));
|
||||
app.use("/datastore", Express.static(Path.join(__dirname, Env.paths.data), {
|
||||
app.use("/datastore", Express.static(Env.paths.data, {
|
||||
maxAge: "0d"
|
||||
}));
|
||||
app.use("/block", Express.static(Path.join(__dirname, Env.paths.block), {
|
||||
|
||||
app.use("/block", Express.static(Env.paths.block, {
|
||||
maxAge: "0d",
|
||||
}));
|
||||
|
||||
app.use("/customize", Express.static(__dirname + '/customize'));
|
||||
app.use("/customize", Express.static(__dirname + '/customize.dist'));
|
||||
app.use("/customize.dist", Express.static(__dirname + '/customize.dist'));
|
||||
app.use(/^\/[^\/]*$/, Express.static('customize'));
|
||||
app.use(/^\/[^\/]*$/, Express.static('customize.dist'));
|
||||
app.use("/customize", Express.static(Path.resolve('customize')));
|
||||
app.use("/customize", Express.static(Path.resolve('customize.dist')));
|
||||
app.use("/customize.dist", Express.static(Path.resolve('customize.dist')));
|
||||
app.use(/^\/[^\/]*$/, Express.static(Path.resolve('customize')));
|
||||
app.use(/^\/[^\/]*$/, Express.static(Path.resolve('customize.dist')));
|
||||
|
||||
// if dev mode: never cache
|
||||
var cacheString = function () {
|
||||
|
@ -216,7 +217,7 @@ var makeRouteCache = function (template, cacheName) {
|
|||
};
|
||||
};
|
||||
|
||||
var serveConfig = makeRouteCache(function (host) {
|
||||
var serveConfig = makeRouteCache(function () {
|
||||
return [
|
||||
'define(function(){',
|
||||
'return ' + JSON.stringify({
|
||||
|
@ -244,10 +245,10 @@ var serveConfig = makeRouteCache(function (host) {
|
|||
accounts_api: Env.accounts_api,
|
||||
}, null, '\t'),
|
||||
'});'
|
||||
].join(';\n')
|
||||
].join(';\n');
|
||||
}, 'configCache');
|
||||
|
||||
var serveBroadcast = makeRouteCache(function (host) {
|
||||
var serveBroadcast = makeRouteCache(function () {
|
||||
var maintenance = Env.maintenance;
|
||||
if (maintenance && maintenance.end && maintenance.end < (+new Date())) {
|
||||
maintenance = undefined;
|
||||
|
@ -260,21 +261,21 @@ var serveBroadcast = makeRouteCache(function (host) {
|
|||
maintenance: maintenance
|
||||
}, null, '\t'),
|
||||
'});'
|
||||
].join(';\n')
|
||||
].join(';\n');
|
||||
}, 'broadcastCache');
|
||||
|
||||
app.get('/api/config', serveConfig);
|
||||
app.get('/api/broadcast', serveBroadcast);
|
||||
|
||||
var define = function (obj) {
|
||||
var defineBlock = function (obj) {
|
||||
return `define(function (){
|
||||
return ${JSON.stringify(obj, null, '\t')};
|
||||
});`
|
||||
});`;
|
||||
};
|
||||
|
||||
app.get('/api/instance', function (req, res) { // XXX use caching?
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.send(define({
|
||||
res.send(defineBlock({
|
||||
name: Env.instanceName,
|
||||
description: Env.instanceDescription,
|
||||
location: Env.instanceJurisdiction,
|
||||
|
@ -282,10 +283,10 @@ app.get('/api/instance', function (req, res) { // XXX use caching?
|
|||
}));
|
||||
});
|
||||
|
||||
var four04_path = Path.resolve(__dirname + '/customize.dist/404.html');
|
||||
var fivehundred_path = Path.resolve(__dirname + '/customize.dist/500.html');
|
||||
var custom_four04_path = Path.resolve(__dirname + '/customize/404.html');
|
||||
var custom_fivehundred_path = Path.resolve(__dirname + '/customize/500.html');
|
||||
var four04_path = Path.resolve('customize.dist/404.html');
|
||||
var fivehundred_path = Path.resolve('customize.dist/500.html');
|
||||
var custom_four04_path = Path.resolve('customize/404.html');
|
||||
var custom_fivehundred_path = Path.resolve('/customize/500.html');
|
||||
|
||||
var send404 = function (res, path) {
|
||||
if (!path && path !== four04_path) { path = four04_path; }
|
||||
|
@ -321,7 +322,7 @@ app.get('/api/updatequota', function (req, res) {
|
|||
});
|
||||
});
|
||||
|
||||
app.get('/api/profiling', function (req, res, next) {
|
||||
app.get('/api/profiling', function (req, res) {
|
||||
if (!Env.enableProfiling) { return void send404(res); }
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.send(JSON.stringify({
|
||||
|
@ -329,13 +330,13 @@ app.get('/api/profiling', function (req, res, next) {
|
|||
}));
|
||||
});
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
app.use(function (req, res) {
|
||||
res.status(404);
|
||||
send404(res, custom_four04_path);
|
||||
});
|
||||
|
||||
// default message for thrown errors in ExpressJS routes
|
||||
app.use(function (err, req, res, next) {
|
||||
app.use(function (err, req, res) {
|
||||
Env.Log.error('EXPRESSJS_ROUTING', {
|
||||
error: err.stack || err,
|
||||
});
|
||||
|
@ -346,7 +347,7 @@ app.use(function (err, req, res, next) {
|
|||
var httpServer = Env.httpServer = Http.createServer(app);
|
||||
|
||||
nThen(function (w) {
|
||||
Fs.exists(__dirname + "/customize", w(function (e) {
|
||||
Fs.exists(Path.resolve("customize"), w(function (e) {
|
||||
if (e) { return; }
|
||||
console.log("CryptPad is customizable, see customize.dist/readme.md for details");
|
||||
}));
|
||||
|
@ -377,7 +378,7 @@ nThen(function (w) {
|
|||
Http.createServer(app).listen(Env.httpSafePort, Env.httpAddress, w());
|
||||
}
|
||||
}).nThen(function () {
|
||||
var wsConfig = { server: httpServer };
|
||||
//var wsConfig = { server: httpServer };
|
||||
|
||||
// Initialize logging then start the API server
|
||||
require("./lib/log").create(config, function (_log) {
|
||||
|
|
|
@ -105,7 +105,7 @@ define([
|
|||
'cp-admin-update-available',
|
||||
'cp-admin-checkup',
|
||||
'cp-admin-block-daily-check',
|
||||
//'cp-admin-provide-aggregate-statistics',
|
||||
'cp-admin-provide-aggregate-statistics',
|
||||
'cp-admin-list-my-instance',
|
||||
|
||||
'cp-admin-consent-to-contact',
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
.cp-small { display: none; }
|
||||
}
|
||||
|
||||
.flatpickr-calendar {
|
||||
z-index: 100001 !important; // Alertify is 100000
|
||||
}
|
||||
#cp-sidebarlayout-container #cp-sidebarlayout-rightside {
|
||||
padding: 0;
|
||||
& > div {
|
||||
|
@ -101,6 +104,21 @@
|
|||
color: @cryptpad_text_col !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tui-full-calendar-floating-layer.cp-calendar-popup-flex {
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
.tui-full-calendar-popup {
|
||||
width: 540px !important;
|
||||
}
|
||||
}
|
||||
#tui-full-calendar-popup-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
.tui-full-calendar-timegrid-timezone {
|
||||
background-color: @cp_sidebar-right-bg !important;
|
||||
|
@ -116,13 +134,30 @@
|
|||
border-color: @cp_calendar-border !important;
|
||||
|
||||
}
|
||||
.tui-full-calendar-popup {
|
||||
border-radius: @variables_radius_L;
|
||||
}
|
||||
.tui-full-calendar-popup-container {
|
||||
background: @cp_flatpickr-bg;
|
||||
color: @cryptpad_text_col;
|
||||
border-radius: @variables_radius;
|
||||
border-radius: @variables_radius_L;
|
||||
font-weight: normal;
|
||||
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox) {
|
||||
display: none;
|
||||
}
|
||||
.tui-full-calendar-popup-detail-item {
|
||||
a {
|
||||
color: @cryptpad_color_link;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.tui-full-calendar-section-button-save {
|
||||
height: 40px;
|
||||
.btn-primary { // Update button
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
li.tui-full-calendar-popup-section-item {
|
||||
padding: 0 6px;
|
||||
|
@ -186,6 +221,9 @@
|
|||
width: 100%;
|
||||
height: 32px;
|
||||
border-radius: @variables_radius;
|
||||
input[type="checkbox"].tui-full-calendar-checkbox-square:checked + span {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABbmlDQ1BpY2MAACiRdZHNK0RRGMZ/Zoh8NIqFNItZDFmMEiVLxsJmkgZlsLn3zpe6c93uvZNkq2wspizExtfCf8BW2VJKkZKs/AG+Npqu97hqJM7t3PfXc87zds5zIJQyjZJbPwAly3PSE8nYXGY+1vhEMy20EyaqGa49NjWV4t/xfkOdqtf9qtf/+/4cLdmca0Bdk/CwYTue8KhwasWzFW8KdxpFLSu8L5xw5IDCF0rXA35UXAj4VbEzkx6HkOoZK/xg/QcbRack3CccL5ll4/s86iatOWt2Wmq3zCguaSZIEkOnzBImHv1SLcnsb9/Al2+SZfEY8rdZxRFHgaJ4E6KWpWtOal70nHwmqyr333m6+aHBoHtrEhoefP+lBxq3oFrx/Y8D368eQvgezqyaf1lyGnkTvVLT4nsQWYeT85qmb8PpBnTd2ZqjfUlhmaF8Hp6PoS0DHVfQvBBk9b3O0S3MrMkTXcLOLvTK/sjiJ6CLZ94KREMsAAAACXBIWXMAAAsSAAALEgHS3X78AAABHUlEQVQoFWNkaP//n4EMwESGHrAW6mtkZvz3G2g03BsBagz/nRUQ7sNp49//TJ+Byv6BlMbrMjCsC2Jg3BbKwOAgB9GMolGAg4GBESIOIrmA+A9I03xvFHGwCrhGkDOe5TAw9LvAFbEn6jEwwTT9BtodvJ7h/4FHYH0MLBCKgaHTgYGBE8jLN2FgYAJae+4FA8NcLwZWkAtAmoLWMTBsuYNwECMsHrVEGBj2RzEwiIEciATANgE1bb6DJAgMNLhTr71hYHBcxsDw+htCAQ5NIAU/4RpBPKjmf2++MfwHaQpZD7cJyEMB3+BORRL+rybE8FOEk4Hj2FO4KMgdrFAMitun2DTCVSMxYAkBFFYg9j94qCIpwsZEil5wyDIDAAXIUsnSKmq7AAAAAElFTkSuQmCC);
|
||||
}
|
||||
.tui-full-calendar-ic-checkbox {
|
||||
margin-left: 5px;
|
||||
border-radius: 2px;
|
||||
|
@ -196,6 +234,7 @@
|
|||
.tui-full-calendar-popup-detail {
|
||||
font: @colortheme_app-font;
|
||||
color: @cryptpad_text_col;
|
||||
box-shadow: @cryptpad_ui_shadow;
|
||||
.tui-full-calendar-popup-container {
|
||||
padding-bottom: 17px;
|
||||
}
|
||||
|
@ -203,28 +242,44 @@
|
|||
font-size: 14px;
|
||||
}
|
||||
.tui-full-calendar-section-button {
|
||||
margin-top: 10px;
|
||||
border: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: start;
|
||||
button {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.tui-full-calendar-popup-top-line {
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
height: 10px;
|
||||
}
|
||||
.tui-full-calendar-popup-vertical-line {
|
||||
visibility: hidden;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-recurrence-label, .cp-notif-label {
|
||||
color: @cryptpad_text_col;
|
||||
margin-right: 1rem;
|
||||
i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-calendar-recurrence-container {
|
||||
margin-top: 1rem;
|
||||
.cp-calendar-rec-translated-str {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-calendar-add-notif {
|
||||
flex-flow: column;
|
||||
align-items: baseline !important;
|
||||
margin: 10px 0;
|
||||
.cp-notif-label {
|
||||
color: @cp_sidebar-hint;
|
||||
margin-right: 20px;
|
||||
}
|
||||
margin: 1rem 0;
|
||||
* {
|
||||
font-size: @colortheme_app-font-size;
|
||||
font-weight: normal;
|
||||
|
@ -234,34 +289,58 @@
|
|||
}
|
||||
.cp-calendar-notif-list-container {
|
||||
margin-bottom: 10px;
|
||||
.cp-notif-label {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
.cp-calendar-notif-list {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
.cp-notif-entry {
|
||||
margin-bottom: 2px;
|
||||
border-radius: @variables_radius;
|
||||
background-color: fade(@cryptpad_text_col, 10%);
|
||||
padding: 0.25rem;
|
||||
.cp-notif-value {
|
||||
width: 170px;
|
||||
display: inline-flex;
|
||||
line-height: 30px;
|
||||
vertical-align: middle;
|
||||
.cp-before {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
span:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
margin: 0px 5px;
|
||||
}
|
||||
.btn-danger-outline {
|
||||
margin-right: 0px !important;
|
||||
background-color: transparent;
|
||||
color: @cryptpad_text_col;
|
||||
border-color: @cryptpad_text_col;
|
||||
&:hover {
|
||||
color: @cp_buttons-red-color;
|
||||
background-color: @cp_buttons-red;
|
||||
border-color: @cp_buttons-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-notif-empty {
|
||||
display: none;
|
||||
margin-bottom: 2px;
|
||||
border-radius: @variables_radius;
|
||||
background-color: fade(@cryptpad_text_col, 10%);
|
||||
padding: 0.25rem 0.5rem;
|
||||
line-height: 30px;
|
||||
}
|
||||
.cp-calendar-notif-list:empty ~ .cp-notif-empty {
|
||||
display: block;
|
||||
}
|
||||
.cp-calendar-notif-form {
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
// margin-bottom: 20px;
|
||||
input {
|
||||
width: 80px;
|
||||
margin-right: 5px;
|
||||
|
@ -270,13 +349,118 @@
|
|||
}
|
||||
|
||||
.cp-calendar-close {
|
||||
top: 17px;
|
||||
right: 17px;
|
||||
height: auto;
|
||||
margin-right: 0px;
|
||||
line-height: initial;
|
||||
border: 1px solid;
|
||||
&:not(:hover) {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.cp-calendar-rec-inline, .cp-calendar-rec-block {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.cp-calendar-rec-inline {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
& > *:not(:first-child) { margin-left: 5px; }
|
||||
.cp-dropdown-container {
|
||||
position: unset;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 80px !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.cp-checkmark {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
.cp-calendar-rec-block {
|
||||
.cp-calendar-rec-block-title {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
.cp-radio {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
input[type="radio"]:not(:checked) ~ .cp-checkmark-label {
|
||||
input {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
.cp-checkmark-label {
|
||||
& > *:not(:first-child) { margin-left: 5px; }
|
||||
width: 100%;
|
||||
//height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > input {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
input {
|
||||
display: inline;
|
||||
height: 24px !important;
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 200px !important;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 80px !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
#cp-calendar-rec-monthly-pick ~ .cp-checkmark-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > span {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
button.cp-calendar-pick-el {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
div.cp-calendar-weekly-pick {
|
||||
button {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
div.cp-calendar-monthly-pick {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
& > div {
|
||||
display: flex;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
button {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
&.lastday {
|
||||
width: 115px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tui-full-calendar-ic-repeat-b {
|
||||
display: none;
|
||||
& ~ * {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#cp-toolbar .cp-calendar-browse {
|
||||
|
@ -395,6 +579,7 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: @variables_radius;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
&.cp-active {
|
||||
background-color: @cp_sidebar-left-item-bg;
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
// Calendars will be exported using this format instead of plain text.
|
||||
define([
|
||||
'/customize/pages.js',
|
||||
], function (Pages) {
|
||||
'/common/common-util.js',
|
||||
'/calendar/recurrence.js'
|
||||
], function (Pages, Util, Rec) {
|
||||
var module = {};
|
||||
|
||||
var getICSDate = function (str) {
|
||||
|
@ -57,24 +59,63 @@ define([
|
|||
var data = content[uid];
|
||||
// DTSTAMP: now...
|
||||
// UID: uid
|
||||
var getDT = function (data) {
|
||||
var start, end;
|
||||
if (data.isAllDay && data.startDay && data.endDay) {
|
||||
if (data.isAllDay) {
|
||||
var startDate = new Date(data.start);
|
||||
var endDate = new Date(data.end);
|
||||
data.startDay = data.startDay || (startDate.getFullYear() + '-' + (startDate.getMonth()+1) + '-' + startDate.getDate());
|
||||
data.endDay = data.endDay || (endDate.getFullYear() + '-' + (endDate.getMonth()+1) + '-' + endDate.getDate());
|
||||
start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
|
||||
end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
|
||||
} else {
|
||||
start = "DTSTART:"+getICSDate(data.start);
|
||||
end = "DTEND:"+getICSDate(data.end);
|
||||
}
|
||||
return {
|
||||
start: start,
|
||||
end: end
|
||||
};
|
||||
};
|
||||
|
||||
Array.prototype.push.apply(ICS, [
|
||||
var getRRule = function (data) {
|
||||
if (!data.recurrenceRule || !data.recurrenceRule.freq) { return; }
|
||||
var r = data.recurrenceRule;
|
||||
var rrule = "RRULE:";
|
||||
rrule += "FREQ="+r.freq.toUpperCase();
|
||||
Object.keys(r).forEach(function (k) {
|
||||
if (k === "freq") { return; }
|
||||
if (k === "by") {
|
||||
Object.keys(r.by).forEach(function (_k) {
|
||||
rrule += ";BY"+_k.toUpperCase()+"="+r.by[_k];
|
||||
});
|
||||
return;
|
||||
}
|
||||
rrule += ";"+k.toUpperCase()+"="+r[k];
|
||||
});
|
||||
return rrule;
|
||||
};
|
||||
|
||||
|
||||
|
||||
var addEvent = function (arr, data, recId) {
|
||||
var uid = data.id;
|
||||
var dt = getDT(data);
|
||||
var start = dt.start;
|
||||
var end = dt.end;
|
||||
var rrule = getRRule(data);
|
||||
|
||||
Array.prototype.push.apply(arr, [
|
||||
'BEGIN:VEVENT',
|
||||
'DTSTAMP:'+getICSDate(+new Date()),
|
||||
'UID:'+uid,
|
||||
start,
|
||||
end,
|
||||
recId,
|
||||
rrule,
|
||||
'SUMMARY:'+ data.title,
|
||||
'LOCATION:'+ data.location,
|
||||
]);
|
||||
].filter(Boolean));
|
||||
|
||||
if (Array.isArray(data.reminders)) {
|
||||
data.reminders.forEach(function (valueMin) {
|
||||
|
@ -91,7 +132,7 @@ define([
|
|||
if (hours || minutes || seconds) {
|
||||
str += "T" + hours + "H" + minutes + "M" + seconds + "S";
|
||||
}
|
||||
Array.prototype.push.apply(ICS, [
|
||||
Array.prototype.push.apply(arr, [
|
||||
'BEGIN:VALARM',
|
||||
'ACTION:DISPLAY',
|
||||
'DESCRIPTION:This is an event reminder',
|
||||
|
@ -102,15 +143,113 @@ define([
|
|||
}
|
||||
|
||||
if (Array.isArray(data.cp_hidden)) {
|
||||
Array.prototype.push.apply(ICS, data.cp_hidden);
|
||||
Array.prototype.push.apply(arr, data.cp_hidden);
|
||||
}
|
||||
|
||||
ICS.push('END:VEVENT');
|
||||
arr.push('END:VEVENT');
|
||||
};
|
||||
|
||||
|
||||
var applyChanges = function (base, changes) {
|
||||
var applyDiff = function (obj, k) {
|
||||
var diff = obj[k]; // Diff is always compared to origin start/end
|
||||
var d = new Date(base[k]);
|
||||
d.setDate(d.getDate() + diff.d);
|
||||
d.setHours(d.getHours() + diff.h);
|
||||
d.setMinutes(d.getMinutes() + diff.m);
|
||||
base[k] = +d;
|
||||
};
|
||||
Object.keys(changes || {}).forEach(function (k) {
|
||||
if (k === "start" || k === "end") {
|
||||
return applyDiff(changes, k);
|
||||
}
|
||||
base[k] = changes[k];
|
||||
});
|
||||
};
|
||||
|
||||
var prev = data;
|
||||
|
||||
// Check if we have "one-time" or "from date" updates.
|
||||
// "One-time" updates will be added accordingly to the ICS specs
|
||||
// "From date" updates will be added as new events and will add
|
||||
// an "until" value to the initial event's RRULE
|
||||
var toAdd = [];
|
||||
if (data.recurrenceRule && data.recurrenceRule.freq && data.recUpdate) {
|
||||
var ru = data.recUpdate;
|
||||
var _all = {};
|
||||
var duration = data.end - data.start;
|
||||
|
||||
var all = Rec.getAllOccurrences(data); // "false" if infinite
|
||||
|
||||
Object.keys(ru.from || {}).forEach(function (d) {
|
||||
if (!Object.keys(ru.from[d] || {}).length) { return; }
|
||||
_all[d] = _all[d] || {};
|
||||
_all[d].from = ru.from[d];
|
||||
});
|
||||
Object.keys(ru.one || {}).forEach(function (d) {
|
||||
if (!Object.keys(ru.one[d] || {}).length) { return; }
|
||||
_all[d] = _all[d] || {};
|
||||
_all[d].one = ru.one[d];
|
||||
});
|
||||
Object.keys(_all).sort(function (a, b) {
|
||||
return Number(a) - Number(b);
|
||||
}).forEach(function (d) {
|
||||
d = Number(d);
|
||||
var r = prev.recurrenceRule;
|
||||
|
||||
// This rule won't apply if we've reached "until" or "count"
|
||||
var idx = all && all.indexOf(d);
|
||||
if (all && idx === -1) {
|
||||
// Make sure we don't have both count and until
|
||||
if (all.length === r.count) { delete r.until; }
|
||||
else { delete r.count; }
|
||||
return;
|
||||
}
|
||||
|
||||
var ud = _all[d];
|
||||
|
||||
if (ud.from) { // "From" updates are not supported by ICS: make a new event
|
||||
var _new = Util.clone(prev);
|
||||
r.until = getICSDate(d - 1); // Stop previous recursion
|
||||
delete r.count;
|
||||
addEvent(ICS, prev, null); // Add previous event
|
||||
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
|
||||
toAdd = [];
|
||||
prev = _new;
|
||||
if (all) { all = all.slice(idx); }
|
||||
|
||||
// if we updated the recurrence rule, count is reset, nothing to do
|
||||
// if we didn't update the recurrence, we need to fix the count
|
||||
var _r = _new.recurrenceRule;
|
||||
if (all && !ud.from.recurrenceRule && _r && _r.count) {
|
||||
_r.count -= idx;
|
||||
}
|
||||
|
||||
prev.start = d;
|
||||
prev.end = d + duration;
|
||||
prev.id = Util.uid();
|
||||
applyChanges(prev, ud.from);
|
||||
duration = prev.end - prev.start;
|
||||
}
|
||||
if (ud.one) { // Add update
|
||||
var _one = Util.clone(prev);
|
||||
_one.start = d;
|
||||
_one.end = d + duration;
|
||||
applyChanges(_one, ud.one);
|
||||
var recId = "RECURRENCE-ID:"+getICSDate(+d);
|
||||
delete _one.recurrenceRule;
|
||||
addEvent(toAdd, _one, recId); // Add updated event
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addEvent(ICS, prev);
|
||||
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
|
||||
});
|
||||
|
||||
ICS.push('END:VCALENDAR');
|
||||
|
||||
return new Blob([ ICS.join('\n') ], { type: 'text/calendar;charset=utf-8' });
|
||||
return new Blob([ ICS.join('\r\n') ], { type: 'text/calendar;charset=utf-8' });
|
||||
};
|
||||
|
||||
module.import = function (content, id, cb) {
|
||||
|
@ -171,7 +310,7 @@ define([
|
|||
}
|
||||
|
||||
// Store other properties
|
||||
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp'];
|
||||
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp', 'rrule', 'recurrence-id'];
|
||||
var hidden = [];
|
||||
ev.getAllProperties().forEach(function (p) {
|
||||
if (used.indexOf(p.name) !== -1) { return; }
|
||||
|
@ -192,8 +331,25 @@ define([
|
|||
if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); }
|
||||
});
|
||||
|
||||
// Get recurrence rule
|
||||
var rrule = ev.getFirstPropertyValue('rrule');
|
||||
var rec;
|
||||
if (rrule && rrule.freq) {
|
||||
rec = {};
|
||||
rec.freq = rrule.freq.toLowerCase();
|
||||
if (rrule.interval) { rec.interval = rrule.interval; }
|
||||
if (rrule.count) { rec.count = rrule.count; }
|
||||
if (Object.keys(rrule).includes('wkst')) { rec.wkst = (rrule.wkst + 6) % 7; }
|
||||
if (rrule.until) { rec.until = +new Date(rrule.until); }
|
||||
Object.keys(rrule.parts || {}).forEach(function (k) {
|
||||
rec.by = rec.by || {};
|
||||
var _k = k.toLowerCase().slice(2); // "BYDAY" ==> "day"
|
||||
rec.by[_k] = rrule.parts[k];
|
||||
});
|
||||
}
|
||||
|
||||
// Create event
|
||||
res[uid] = {
|
||||
var obj = {
|
||||
calendarId: id,
|
||||
id: uid,
|
||||
category: 'time',
|
||||
|
@ -203,16 +359,49 @@ define([
|
|||
start: start,
|
||||
end: end,
|
||||
reminders: reminders,
|
||||
cp_hidden: hidden
|
||||
cp_hidden: hidden,
|
||||
};
|
||||
if (rec) { obj.recurrenceRule = rec; }
|
||||
|
||||
if (!hidden.length) { delete res[uid].cp_hidden; }
|
||||
if (!reminders.length) { delete res[uid].reminders; }
|
||||
if (!hidden.length) { delete obj.cp_hidden; }
|
||||
if (!reminders.length) { delete obj.reminders; }
|
||||
|
||||
var recId = ev.getFirstPropertyValue('recurrence-id');
|
||||
if (recId) {
|
||||
setTimeout(function () {
|
||||
if (!res[uid]) { return; }
|
||||
var old = res[uid];
|
||||
var time = +new Date(recId);
|
||||
var diff = {};
|
||||
var from = {};
|
||||
Object.keys(obj).forEach(function (k) {
|
||||
if (JSON.stringify(old[k]) === JSON.stringify(obj[k])) { return; }
|
||||
if (['start','end'].includes(k)) {
|
||||
diff[k] = Rec.diffDate(old[k], obj[k]);
|
||||
return;
|
||||
}
|
||||
if (k === "recurrenceRule") {
|
||||
from[k] = obj[k];
|
||||
return;
|
||||
}
|
||||
diff[k] = obj[k];
|
||||
});
|
||||
old.recUpdate = old.recUpdate || {one:{},from:{}};
|
||||
if (Object.keys(from).length) { old.recUpdate.from[time] = from; }
|
||||
if (Object.keys(diff).length) { old.recUpdate.one[time] = diff; }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res[uid] = obj;
|
||||
});
|
||||
|
||||
// setTimeout to make sure we call back after the "recurrence-id" setTimeout
|
||||
// are called
|
||||
setTimeout(function () {
|
||||
cb(null, res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return module;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,17 +6,27 @@ define([
|
|||
'/common/sframe-common-outer.js',
|
||||
], function (nThen, ApiConfig, DomReady, SFCommonO) {
|
||||
|
||||
// Loaded in load #2
|
||||
var hash, href;
|
||||
nThen(function (waitFor) {
|
||||
DomReady.onReady(waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
SFCommonO.initIframe(waitFor);
|
||||
var obj = SFCommonO.initIframe(waitFor, true);
|
||||
href = obj.href;
|
||||
hash = obj.hash;
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
var addData = function (meta) {
|
||||
meta.calendarHash = Boolean(window.location.hash);
|
||||
var addData = function (meta, Cryptpad, user, Utils) {
|
||||
if (hash) {
|
||||
var parsed = Utils.Hash.parsePadUrl(href);
|
||||
if (parsed.hashData && parsed.hashData.newPadOpts) {
|
||||
meta.calendarOpts = Utils.Hash.decodeDataOptions(parsed.hashData.newPadOpts);
|
||||
}
|
||||
}
|
||||
meta.calendarHash = hash;
|
||||
};
|
||||
SFCommonO.start({
|
||||
addData: addData,
|
||||
hash: hash,
|
||||
href: href,
|
||||
noRealtime: true,
|
||||
cache: true,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,869 @@
|
|||
define([
|
||||
'/common/common-util.js',
|
||||
], function (Util) {
|
||||
var Rec = {};
|
||||
|
||||
var debug = function () {};
|
||||
|
||||
// Get week number with any "WKST" (firts day of the week)
|
||||
// Week 1 is the first week of the year containing at least 4 days in this year
|
||||
// It depends on which day is considered the first day of the week (default Monday)
|
||||
// In our case, wkst is a number matching the JS rule: 0 == Sunday
|
||||
var getWeekNo = Rec.getWeekNo = function (date, wkst) {
|
||||
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
|
||||
|
||||
var newYear = new Date(date.getFullYear(),0,1);
|
||||
var day = newYear.getDay() - wkst; //the day of week the year begins on
|
||||
day = (day >= 0 ? day : day + 7);
|
||||
var daynum = Math.floor((date.getTime() - newYear.getTime())/86400000) + 1;
|
||||
var weeknum;
|
||||
// Week 1 / week 53
|
||||
if (day < 4) {
|
||||
weeknum = Math.floor((daynum+day-1)/7) + 1;
|
||||
if (weeknum > 52) {
|
||||
var nYear = new Date(date.getFullYear() + 1,0,1);
|
||||
var nday = nYear.getDay() - wkst;
|
||||
nday = nday >= 0 ? nday : nday + 7;
|
||||
weeknum = nday < 4 ? 1 : 53;
|
||||
}
|
||||
}
|
||||
else {
|
||||
weeknum = Math.floor((daynum+day-1)/7);
|
||||
}
|
||||
return weeknum;
|
||||
};
|
||||
|
||||
var getYearDay = function (date) {
|
||||
var start = new Date(date.getFullYear(), 0, 0);
|
||||
var diff = (date - start) +
|
||||
((start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000);
|
||||
var oneDay = 1000 * 60 * 60 * 24;
|
||||
return Math.floor(diff / oneDay);
|
||||
};
|
||||
var setYearDay = function (date, day) {
|
||||
if (typeof(day) !== "number" || Math.abs(day) < 1 || Math.abs(day) > 366) { return; }
|
||||
if (day < 0) {
|
||||
var max = getYearDay(new Date(date.getFullYear(), 11, 31));
|
||||
day = max + day + 1;
|
||||
}
|
||||
date.setMonth(0);
|
||||
date.setDate(day);
|
||||
return true;
|
||||
};
|
||||
|
||||
var getEndData = function (s, e) {
|
||||
if (s > e) { return void console.error("Wrong data"); }
|
||||
var days;
|
||||
if (e.getFullYear() === s.getFullYear()) {
|
||||
days = getYearDay(e) - getYearDay(s);
|
||||
} else { // eYear < sYear
|
||||
var tmp = new Date(s.getFullYear(), 11, 31);
|
||||
var d1 = getYearDay(tmp) - getYearDay(s); // Number of days before December 31st
|
||||
var de = getYearDay(e);
|
||||
days = d1 + de;
|
||||
while ((tmp.getFullYear()+1) < e.getFullYear()) {
|
||||
tmp.setFullYear(tmp.getFullYear()+1);
|
||||
days += getYearDay(tmp);
|
||||
}
|
||||
}
|
||||
return {
|
||||
h: e.getHours(),
|
||||
m: e.getMinutes(),
|
||||
days: days
|
||||
};
|
||||
};
|
||||
var setEndData = function (s, e, data) {
|
||||
e.setTime(+s);
|
||||
if (!data) { return; }
|
||||
e.setHours(data.h);
|
||||
e.setMinutes(data.m);
|
||||
e.setSeconds(0);
|
||||
e.setDate(s.getDate() + data.days);
|
||||
};
|
||||
|
||||
var DAYORDER = Rec.DAYORDER = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
|
||||
var getDayData = function (str) {
|
||||
var pos = Number(str.slice(0,-2));
|
||||
var day = DAYORDER.indexOf(str.slice(-2));
|
||||
return pos ? [pos, day] : day;
|
||||
};
|
||||
|
||||
var goToFirstWeekDay = function (date, wkst) {
|
||||
var d = date.getDay();
|
||||
wkst = typeof(wkst) === "number" ? wkst : 1;
|
||||
if (d >= wkst) {
|
||||
date.setDate(date.getDate() - (d-wkst));
|
||||
} else {
|
||||
date.setDate(date.getDate() - (7+d-wkst));
|
||||
}
|
||||
};
|
||||
|
||||
var getDateStr = function (date) {
|
||||
return date.getFullYear() + '-' + (date.getMonth()+1) + '-' + date.getDate();
|
||||
};
|
||||
var FREQ = {};
|
||||
FREQ['daily'] = function (s, i) {
|
||||
s.setDate(s.getDate()+i);
|
||||
};
|
||||
FREQ['weekly'] = function (s,i) {
|
||||
s.setDate(s.getDate()+(i*7));
|
||||
};
|
||||
FREQ['monthly'] = function (s,i) {
|
||||
s.setMonth(s.getMonth()+i);
|
||||
};
|
||||
FREQ['yearly'] = function (s,i) {
|
||||
s.setFullYear(s.getFullYear()+i);
|
||||
};
|
||||
|
||||
// EXPAND is used to create iterations added from a BYxxx rule
|
||||
// dateA is the start date and b is the number or id of the BYxxx rule item
|
||||
var EXPAND = {};
|
||||
EXPAND['month'] = function (dateS, origin, b) {
|
||||
var oS = new Date(origin.start);
|
||||
var a = dateS.getMonth() + 1;
|
||||
var toAdd = (b-a+12)%12;
|
||||
var m = dateS.getMonth() + toAdd;
|
||||
dateS.setMonth(m);
|
||||
dateS.setDate(oS.getDate());
|
||||
if (dateS.getMonth() !== m) { return; } // Day 31 may move us to the next month
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
EXPAND['weekno'] = function (dateS, origin, week, rule) {
|
||||
var wkst = rule && rule.wkst;
|
||||
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
|
||||
var oS = new Date(origin.start);
|
||||
|
||||
var lastD = new Date(dateS.getFullYear(), 11, 31); // December 31st
|
||||
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
|
||||
|
||||
var doubleOne = lastW === 1;
|
||||
if (lastW === 1) { lastW = 52; }
|
||||
|
||||
var a = getWeekNo(dateS, wkst);
|
||||
if (!week || week > lastW) { return false; } // Week 53 may not exist this year
|
||||
|
||||
if (week < 0) { week = lastW + week + 1; } // Turn negative week number into positive
|
||||
|
||||
var toAdd = week - a;
|
||||
var weekS = new Date(+dateS);
|
||||
// Go to the selected week
|
||||
weekS.setDate(weekS.getDate() + (toAdd * 7));
|
||||
goToFirstWeekDay(weekS, wkst);
|
||||
|
||||
// Then make sure we are in the correct start day
|
||||
var all = 'aaaaaaa'.split('').map(function (o, i) {
|
||||
var date = new Date(+weekS);
|
||||
date.setDate(date.getDate() + i);
|
||||
if (date.getFullYear() !== dateS.getFullYear()) { return; }
|
||||
return date.toLocaleDateString() !== oS.toLocaleDateString() && date;
|
||||
}).filter(Boolean);
|
||||
|
||||
// If we're looking for week 1 and the last week is a week 1, add the days
|
||||
if (week === 1 && doubleOne) {
|
||||
goToFirstWeekDay(lastD, wkst);
|
||||
'aaaaaaa'.split('').some(function (o, i) {
|
||||
var date = new Date(+lastD);
|
||||
date.setDate(date.getDate() + i);
|
||||
if (date.toLocaleDateString() === oS.toLocaleDateString()) { return; }
|
||||
if (date.getFullYear() > dateS.getFullYear()) { return true; }
|
||||
all.push(date);
|
||||
});
|
||||
}
|
||||
|
||||
return all.length ? all : undefined;
|
||||
};
|
||||
EXPAND['yearday'] = function (dateS, origin, b) {
|
||||
var y = dateS.getFullYear();
|
||||
var state = setYearDay(dateS, b);
|
||||
if (!state) { return; } // Invalid day "b"
|
||||
if (dateS.getFullYear() !== y) { return; } // Day 366 make move us to the next year
|
||||
return true;
|
||||
};
|
||||
EXPAND['monthday'] = function (dateS, origin, b, rule) {
|
||||
if (typeof(b) !== "number" || Math.abs(b) < 1 || Math.abs(b) > 31) { return false; }
|
||||
|
||||
var setMonthDay = function (date, day) {
|
||||
var m = date.getMonth();
|
||||
if (day < 0) {
|
||||
var tmp = new Date(date.getFullYear(), date.getMonth()+1, 0); // Last day
|
||||
day = tmp.getDate() + day + 1;
|
||||
}
|
||||
date.setDate(day);
|
||||
return date.getMonth() === m; // Don't push if day 31 moved us to the next month
|
||||
|
||||
};
|
||||
|
||||
// Monthly events
|
||||
if (rule.freq === 'monthly') {
|
||||
return setMonthDay(dateS, b);
|
||||
}
|
||||
|
||||
var all = 'aaaaaaaaaaaa'.split('').map(function (o, i) {
|
||||
var date = new Date(dateS.getFullYear(), i, 1);
|
||||
var ok = setMonthDay(date, b);
|
||||
return ok ? date : undefined;
|
||||
}).filter(Boolean);
|
||||
return all.length ? all : undefined;
|
||||
};
|
||||
EXPAND['day'] = function (dateS, origin, b, rule) {
|
||||
// Here "b" can be a single day ("TU") or a position and a day ("1MO")
|
||||
var day = getDayData(b);
|
||||
var pos;
|
||||
if (Array.isArray(day)) {
|
||||
pos = day[0];
|
||||
day = day[1];
|
||||
}
|
||||
|
||||
var all = [];
|
||||
if (![0,1,2,3,4,5,6].includes(day)) { return false; }
|
||||
|
||||
var filterPos = function (m) {
|
||||
if (!pos) { return; }
|
||||
|
||||
var _all = [];
|
||||
'aaaaaaaaaaaa'.split('').some(function (a, i) {
|
||||
if (typeof(m) !== "undefined" && i !== m) { return; }
|
||||
|
||||
var _pos;
|
||||
var tmp = all.filter(function (d) {
|
||||
return d.getMonth() === i;
|
||||
});
|
||||
if (pos < 0) {
|
||||
_pos = tmp.length + pos;
|
||||
} else {
|
||||
_pos = pos - 1; // An array starts at 0 but the recurrence rule starts at 1
|
||||
}
|
||||
_all.push(tmp[_pos]);
|
||||
|
||||
return typeof(m) !== "undefined" && i === m;
|
||||
});
|
||||
all = _all.filter(Boolean); // The "5th" {day} won't always exist
|
||||
};
|
||||
|
||||
var tmp;
|
||||
if (rule.freq === 'yearly') {
|
||||
tmp = new Date(+dateS);
|
||||
var y = dateS.getFullYear();
|
||||
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
|
||||
while (tmp.getFullYear() === y) {
|
||||
all.push(new Date(+tmp));
|
||||
tmp.setDate(tmp.getDate()+7);
|
||||
}
|
||||
filterPos();
|
||||
return all;
|
||||
}
|
||||
|
||||
if (rule.freq === 'monthly') {
|
||||
tmp = new Date(+dateS);
|
||||
var m = dateS.getMonth();
|
||||
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
|
||||
while (tmp.getMonth() === m) {
|
||||
all.push(new Date(+tmp));
|
||||
tmp.setDate(tmp.getDate()+7);
|
||||
}
|
||||
filterPos(m);
|
||||
return all;
|
||||
}
|
||||
|
||||
if (rule.freq === 'weekly') {
|
||||
while (dateS.getDay() !== day) { dateS.setDate(dateS.getDate()+1); }
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var LIMIT = {};
|
||||
LIMIT['month'] = function (events, rule) {
|
||||
return events.filter(function (s) {
|
||||
return rule.includes(s.getMonth()+1);
|
||||
});
|
||||
};
|
||||
LIMIT['weekno'] = function (events, weeks, rules) {
|
||||
return events.filter(function (s) {
|
||||
var wkst = rules && rules.wkst;
|
||||
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
|
||||
|
||||
var lastD = new Date(s.getFullYear(), 11, 31); // December 31st
|
||||
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
|
||||
if (lastW === 1) { lastW = 52; }
|
||||
|
||||
var w = getWeekNo(s, wkst);
|
||||
|
||||
return weeks.some(function (week) {
|
||||
if (week > 0) { return week === w; }
|
||||
return w === (lastW + week + 1);
|
||||
});
|
||||
});
|
||||
};
|
||||
LIMIT['yearday'] = function (events, days) {
|
||||
return events.filter(function (s) {
|
||||
var d = getYearDay(s);
|
||||
var max = getYearDay(new Date(s.getFullYear(), 11, 31));
|
||||
|
||||
return days.some(function (day) {
|
||||
if (day > 0) { return day === d; }
|
||||
return d === (max + day + 1);
|
||||
});
|
||||
});
|
||||
};
|
||||
LIMIT['monthday'] = function (events, rule) {
|
||||
return events.filter(function (s) {
|
||||
var r = Util.clone(rule);
|
||||
// Transform the negative monthdays into positive for this specific month
|
||||
r = r.map(function (b) {
|
||||
if (b < 0) {
|
||||
var tmp = new Date(s.getFullYear(), s.getMonth()+1, 0); // Last day
|
||||
b = tmp.getDate() + b + 1;
|
||||
}
|
||||
return b;
|
||||
});
|
||||
return r.includes(s.getDate());
|
||||
});
|
||||
};
|
||||
LIMIT['day'] = function (events, days, rules) {
|
||||
return events.filter(function (s) {
|
||||
var dayStr = s.toLocaleDateString();
|
||||
|
||||
// Check how to handle position in BYDAY rules (last day of the month or the year?)
|
||||
var type = 'yearly';
|
||||
if (rules.freq === 'monthly' ||
|
||||
(rules.freq === 'yearly' && rules.by && rules.by.month)) {
|
||||
type = 'monthly';
|
||||
}
|
||||
|
||||
// Check if this event matches one of the allowed days
|
||||
return days.some(function (r) {
|
||||
// rule elements are strings with pos and day
|
||||
var day = getDayData(r);
|
||||
var pos;
|
||||
if (Array.isArray(day)) {
|
||||
pos = day[0];
|
||||
day = day[1];
|
||||
}
|
||||
if (!pos) {
|
||||
return s.getDay() === day;
|
||||
}
|
||||
|
||||
// If we have a position, we can use EXPAND.day to get the nth {day} of the
|
||||
// year/month and compare if it matches with
|
||||
var d = new Date(s.getFullYear(), s.getMonth(), 1);
|
||||
if (type === 'yearly') { d.setMonth(0); }
|
||||
var res = EXPAND["day"](d, {}, r, {freq: type});
|
||||
return res.some(function (date) {
|
||||
return date.toLocaleDateString() === dayStr;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
LIMIT['setpos'] = function (events, rule) {
|
||||
var init = events.slice();
|
||||
var rules = Util.deduplicateString(rule.slice().map(function (n) {
|
||||
if (n > 0) { return (n-1); }
|
||||
if (n === 0) { return; }
|
||||
return init.length + n;
|
||||
}));
|
||||
return events.filter(function (ev) {
|
||||
var idx = init.indexOf(ev);
|
||||
return rules.includes(idx);
|
||||
});
|
||||
};
|
||||
|
||||
var BYORDER = ['month','weekno','yearday','monthday','day'];
|
||||
var BYDAYORDER = ['month','monthday','day'];
|
||||
|
||||
Rec.getMonthId = function (d) {
|
||||
return d.getFullYear() + '-' + d.getMonth();
|
||||
};
|
||||
var cache = window.CP_calendar_cache = {};
|
||||
var recurringAcross = {};
|
||||
Rec.resetCache = function () {
|
||||
cache = window.CP_calendar_cache = {};
|
||||
recurringAcross = {};
|
||||
};
|
||||
|
||||
var iterate = function (rule, _origin, s) {
|
||||
// "origin" is the original event to detect the start of BYxxx
|
||||
var origin = Util.clone(_origin);
|
||||
var oS = new Date(origin.start);
|
||||
|
||||
var id = origin.id.split('|')[0]; // Use same cache when updating recurrence rule
|
||||
|
||||
// "uid" is used for the cache
|
||||
var uid = s.toLocaleDateString();
|
||||
cache[id] = cache[id] || {};
|
||||
|
||||
var inter = rule.interval || 1;
|
||||
var freq = rule.freq;
|
||||
|
||||
var all = [];
|
||||
var limit = function (byrule, n) {
|
||||
all = LIMIT[byrule](all, n, rule);
|
||||
};
|
||||
var expand = function (byrule) {
|
||||
return function (n) {
|
||||
// Set the start date at the beginning of the current FREQ
|
||||
var _s = new Date(+s);
|
||||
if (rule.freq === 'yearly') {
|
||||
// January 1st
|
||||
_s.setMonth(0);
|
||||
_s.setDate(1);
|
||||
} else if (rule.freq === 'monthly') {
|
||||
_s.setDate(1);
|
||||
} else if (rule.freq === 'weekly') {
|
||||
goToFirstWeekDay(_s, rule.wkst);
|
||||
} else if (rule.freq === 'daily') {
|
||||
// We don't have < byday rules so we can't expand daily rules
|
||||
}
|
||||
|
||||
var add = EXPAND[byrule](_s, origin, n, rule);
|
||||
|
||||
if (!add) { return; }
|
||||
|
||||
if (Array.isArray(add)) {
|
||||
add = add.filter(function (dateS) {
|
||||
return dateS.toLocaleDateString() !== oS.toLocaleDateString();
|
||||
});
|
||||
Array.prototype.push.apply(all, add);
|
||||
} else {
|
||||
if (_s.toLocaleDateString() === oS.toLocaleDateString()) { return; }
|
||||
all.push(_s);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Manage interval for the next iteration
|
||||
var it = Util.once(function () {
|
||||
FREQ[freq](s, inter);
|
||||
});
|
||||
var addDefault = function () {
|
||||
if (freq === "monthly") {
|
||||
s.setDate(15);
|
||||
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
|
||||
s.setDate(28);
|
||||
}
|
||||
|
||||
it();
|
||||
|
||||
var _s = new Date(+s);
|
||||
if (freq === "monthly" || freq === "yearly") {
|
||||
_s.setDate(oS.getDate());
|
||||
if (_s.getDate() !== oS.getDate()) { return; } // If 31st or Feb 29th doesn't exist
|
||||
if (freq === "yearly" && _s.getMonth() !== oS.getMonth()) { return; }
|
||||
|
||||
// FIXME if there is a recUpdate that moves the 31st to the 30th, the event
|
||||
// will still only be displayed on months with 31 days
|
||||
}
|
||||
all.push(_s);
|
||||
};
|
||||
|
||||
if (Array.isArray(cache[id][uid])) {
|
||||
debug('Get cache', id, uid);
|
||||
if (freq === "monthly") {
|
||||
s.setDate(15);
|
||||
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
|
||||
s.setDate(28);
|
||||
}
|
||||
it();
|
||||
return cache[id][uid];
|
||||
}
|
||||
|
||||
if (rule.by && freq === 'yearly') {
|
||||
var order = BYORDER.slice();
|
||||
var monthLimit = false;
|
||||
if (rule.by.weekno || rule.by.yearday || rule.by.monthday || rule.by.day) {
|
||||
order.shift();
|
||||
monthLimit = true;
|
||||
}
|
||||
var first = true;
|
||||
order.forEach(function (_order) {
|
||||
var r = rule.by[_order];
|
||||
if (!r) { return; }
|
||||
if (first) {
|
||||
r.forEach(expand(_order));
|
||||
first = false;
|
||||
} else if (_order === "day") {
|
||||
if (rule.by.yearday || rule.by.monthday || rule.by.weekno) {
|
||||
limit('day', rule.by.day);
|
||||
} else {
|
||||
rule.by.day.forEach(expand('day'));
|
||||
}
|
||||
} else {
|
||||
limit(_order, r);
|
||||
}
|
||||
});
|
||||
if (rule.by.month && monthLimit) {
|
||||
limit('month', rule.by.month);
|
||||
}
|
||||
}
|
||||
if (rule.by && freq === 'monthly') {
|
||||
// We're going to compute all the entries for the coming month
|
||||
if (!rule.by.monthday && !rule.by.day) {
|
||||
addDefault();
|
||||
} else if (rule.by.monthday) {
|
||||
rule.by.monthday.forEach(expand('monthday'));
|
||||
} else if (rule.by.day) {
|
||||
rule.by.day.forEach(expand('day'));
|
||||
}
|
||||
if (rule.by.month) {
|
||||
limit('month', rule.by.month);
|
||||
}
|
||||
if (rule.by.day && rule.by.monthday) {
|
||||
limit('day', rule.by.day);
|
||||
}
|
||||
}
|
||||
if (rule.by && freq === 'weekly') {
|
||||
// We're going to compute all the entries for the coming week
|
||||
if (!rule.by.day) {
|
||||
addDefault();
|
||||
} else {
|
||||
rule.by.day.forEach(expand('day'));
|
||||
}
|
||||
if (rule.by.month) {
|
||||
limit('month', rule.by.month);
|
||||
}
|
||||
}
|
||||
if (rule.by && freq === 'daily') {
|
||||
addDefault();
|
||||
BYDAYORDER.forEach(function (_order) {
|
||||
var r = rule.by[_order];
|
||||
if (!r) { return; }
|
||||
limit(_order, r);
|
||||
});
|
||||
}
|
||||
|
||||
all.sort(function (a, b) {
|
||||
return a-b;
|
||||
});
|
||||
|
||||
if (rule.by && rule.by.setpos) {
|
||||
limit('setpos', rule.by.setpos);
|
||||
}
|
||||
|
||||
if (!rule.by || !Object.keys(rule.by).length) {
|
||||
addDefault();
|
||||
} else {
|
||||
it();
|
||||
}
|
||||
|
||||
|
||||
var done = [];
|
||||
all = all.filter(function (newS) {
|
||||
var start = new Date(+newS).toLocaleDateString();
|
||||
if (done.includes(start)) { return false; }
|
||||
done.push(start);
|
||||
return true;
|
||||
});
|
||||
|
||||
debug('Set cache', id, uid);
|
||||
cache[id][uid] = all;
|
||||
|
||||
return all;
|
||||
};
|
||||
|
||||
var getNextRules = function (obj) {
|
||||
if (!obj.recUpdate) { return []; }
|
||||
var _allRules = {};
|
||||
var _obj = obj.recUpdate.from;
|
||||
Object.keys(_obj || {}).forEach(function (d) {
|
||||
var u = _obj[d];
|
||||
if (u.recurrenceRule) { _allRules[d] = u.recurrenceRule; }
|
||||
});
|
||||
return Object.keys(_allRules).sort(function (a, b) { return Number(a)-Number(b); })
|
||||
.map(function (k) {
|
||||
var r = Util.clone(_allRules[k]);
|
||||
if (!FREQ[r.freq]) { return; }
|
||||
if (r.interval && r.interval < 1) { return; }
|
||||
r._start = Number(k);
|
||||
return r;
|
||||
}).filter(Boolean);
|
||||
};
|
||||
Rec.getRecurring = function (months, events) {
|
||||
if (window.CP_DEV_MODE) { debug = console.warn; }
|
||||
|
||||
var toAdd = [];
|
||||
months.forEach(function (monthId) {
|
||||
// from 1st day of the month at 00:00 to last day at 23:59:59:999
|
||||
var ms = monthId.split('-');
|
||||
var _startMonth = new Date(ms[0], ms[1]);
|
||||
var _endMonth = new Date(+_startMonth);
|
||||
_endMonth.setMonth(_endMonth.getMonth() + 1);
|
||||
_endMonth.setMilliseconds(-1);
|
||||
|
||||
debug('Compute month', _startMonth.toLocaleDateString());
|
||||
|
||||
var rec = events || [];
|
||||
rec.forEach(function (obj) {
|
||||
var _start = new Date(obj.start);
|
||||
var _end = new Date(obj.end);
|
||||
var _origin = obj;
|
||||
var rule = obj.recurrenceRule;
|
||||
if (!rule) { return; }
|
||||
|
||||
var nextRules = getNextRules(obj);
|
||||
var nextRule = nextRules.shift();
|
||||
|
||||
if (_start >= _endMonth) { return; }
|
||||
|
||||
// Check the "until" date of the latest rule we can use and stop now
|
||||
// if the recurrence ends before the current month
|
||||
var until = rule.until;
|
||||
var _nextRules = nextRules.slice();
|
||||
var _nextRule = nextRule;
|
||||
while (_nextRule && _nextRule._start && _nextRule._start < _startMonth) {
|
||||
until = nextRule.until;
|
||||
_nextRule = _nextRules.shift();
|
||||
}
|
||||
if (until < _startMonth) { return; }
|
||||
|
||||
var endData = getEndData(_start, _end);
|
||||
|
||||
if (rule.interval && rule.interval < 1) { return; }
|
||||
if (!FREQ[rule.freq]) { return; }
|
||||
|
||||
/*
|
||||
// Rule examples
|
||||
rule.by = {
|
||||
//month: [1, 4, 5, 8, 12],
|
||||
//weekno: [1, 2, 4, 5, 32, 34, 35, 50],
|
||||
//yearday: [1, 2, 29, 30, -2, -1, 250],
|
||||
//monthday: [1, 2, 3, -3, -2, -1],
|
||||
//day: ["MO", "WE", "FR"],
|
||||
//setpos: [1, 2, -1, -2]
|
||||
};
|
||||
rule.wkst = 0;
|
||||
rule.interval = 2;
|
||||
rule.freq = 'yearly';
|
||||
rule.count = 10;
|
||||
*/
|
||||
debug('Iterate over', obj.title, obj);
|
||||
debug('Use rule', rule);
|
||||
|
||||
var count = rule.count;
|
||||
var c = 1;
|
||||
|
||||
var next = function (start) {
|
||||
var evS = new Date(+start);
|
||||
|
||||
if (count && c >= count) { return; }
|
||||
|
||||
debug('Start iteration', evS.toLocaleDateString());
|
||||
|
||||
var _toAdd = iterate(rule, obj, evS);
|
||||
|
||||
debug('Iteration results', JSON.stringify(_toAdd.map(function (o) { return new Date(o).toLocaleDateString();})));
|
||||
|
||||
// Make sure to continue if the current year doesn't provide any result
|
||||
if (!_toAdd.length) {
|
||||
if (evS.getFullYear() < _startMonth.getFullYear() ||
|
||||
evS < _endMonth) {
|
||||
return void next(evS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var stop = false;
|
||||
var newrule = false;
|
||||
_toAdd.some(function (_newS) {
|
||||
// Make event with correct start and end time
|
||||
var _ev = Util.clone(obj);
|
||||
_ev.id = _origin.id + '|' + (+_newS);
|
||||
var _evS = new Date(+_newS);
|
||||
var _evE = new Date(+_newS);
|
||||
setEndData(_evS, _evE, endData);
|
||||
_ev.start = +_evS;
|
||||
_ev.end = +_evE;
|
||||
_ev._count = c;
|
||||
if (_ev.isAllDay && _ev.startDay) { _ev.startDay = getDateStr(_evS); }
|
||||
if (_ev.isAllDay && _ev.endDay) { _ev.endDay = getDateStr(_evE); }
|
||||
|
||||
if (nextRule && _ev.start === nextRule._start) {
|
||||
newrule = true;
|
||||
}
|
||||
|
||||
var useNewRule = function () {
|
||||
if (!newrule) { return; }
|
||||
debug('Use new rule', nextRule);
|
||||
_ev._count = c;
|
||||
count = nextRule.count;
|
||||
c = 1;
|
||||
evS = +_evS;
|
||||
obj = _ev;
|
||||
rule = nextRule;
|
||||
nextRule = nextRules.shift();
|
||||
};
|
||||
|
||||
|
||||
if (c >= count) { // Limit reached
|
||||
debug(_evS.toLocaleDateString(), 'count');
|
||||
stop = true;
|
||||
return true;
|
||||
}
|
||||
if (_evS >= _endMonth) { // Won't affect us anymore
|
||||
debug(_evS.toLocaleDateString(), 'endMonth');
|
||||
stop = true;
|
||||
return true;
|
||||
}
|
||||
if (rule.until && _evS > rule.until) {
|
||||
debug(_evS.toLocaleDateString(), 'until');
|
||||
stop = true;
|
||||
return true;
|
||||
}
|
||||
if (_evS < _start) { // "Expand" rules may create events before the _start
|
||||
debug(_evS.toLocaleDateString(), 'start');
|
||||
return;
|
||||
}
|
||||
c++;
|
||||
if (_evE < _startMonth) { // Ended before the current month
|
||||
// Nothing to display but continue the recurrence
|
||||
debug(_evS.toLocaleDateString(), 'startMonth');
|
||||
if (newrule) { useNewRule(); }
|
||||
return;
|
||||
}
|
||||
// If a recurring event start and end in different months, make sure
|
||||
// it is only added once
|
||||
if ((_evS < _endMonth && _evE >= _endMonth) ||
|
||||
(_evS < _startMonth && _evE >= _startMonth)) {
|
||||
if (recurringAcross[_ev.id] && recurringAcross[_ev.id].includes(_ev.start)) {
|
||||
return;
|
||||
} else {
|
||||
recurringAcross[_ev.id] = recurringAcross[_ev.id] || [];
|
||||
recurringAcross[_ev.id].push(_ev.start);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add this event
|
||||
toAdd.push(_ev);
|
||||
if (newrule) {
|
||||
useNewRule();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!stop) { next(evS); }
|
||||
};
|
||||
next(_start);
|
||||
debug('Added this month (all events)', toAdd.map(function (ev) {
|
||||
return new Date(ev.start).toLocaleDateString();
|
||||
}));
|
||||
});
|
||||
});
|
||||
return toAdd;
|
||||
};
|
||||
Rec.getAllOccurrences = function (ev) {
|
||||
if (!ev.recurrenceRule) { return [ev.start]; }
|
||||
var r = ev.recurrenceRule;
|
||||
// In case of infinite recursion, we can't get all
|
||||
if (!r.until && !r.count) { return false; }
|
||||
var all = [ev.start];
|
||||
var d = new Date(ev.start);
|
||||
d.setDate(15); // Make sure we won't skip a month if the event starts on day > 28
|
||||
var toAdd = [];
|
||||
|
||||
var i = 0;
|
||||
var check = function () {
|
||||
return r.count ? (all.length < r.count) : (+d <= r.until);
|
||||
};
|
||||
while ((toAdd = Rec.getRecurring([Rec.getMonthId(d)], [ev])) && check() && i < (r.count*12)) {
|
||||
Array.prototype.push.apply(all, toAdd.map(function (_ev) { return _ev.start; }));
|
||||
d.setMonth(d.getMonth() + 1);
|
||||
i++;
|
||||
}
|
||||
|
||||
return all;
|
||||
};
|
||||
|
||||
Rec.diffDate = function (oldTime, newTime) {
|
||||
var n = new Date(newTime);
|
||||
var o = new Date(oldTime);
|
||||
|
||||
// Diff Days
|
||||
var d = 0;
|
||||
var mult = n < o ? -1 : 1;
|
||||
while (n.toLocaleDateString() !== o.toLocaleDateString() || mult >= 10000) {
|
||||
n.setDate(n.getDate() - mult);
|
||||
d++;
|
||||
}
|
||||
d = mult * d;
|
||||
|
||||
// Diff hours
|
||||
n = new Date(newTime);
|
||||
var h = n.getHours() - o.getHours();
|
||||
|
||||
// Diff minutes
|
||||
var m = n.getMinutes() - o.getMinutes();
|
||||
|
||||
return {
|
||||
d: d,
|
||||
h: h,
|
||||
m: m
|
||||
};
|
||||
};
|
||||
|
||||
var sortUpdate = function (obj) {
|
||||
return Object.keys(obj).sort(function (d1, d2) {
|
||||
return Number(d1) - Number(d2);
|
||||
});
|
||||
};
|
||||
Rec.applyUpdates = function (events) {
|
||||
events.forEach(function (ev) {
|
||||
ev.raw = {
|
||||
start: ev.start,
|
||||
end: ev.end,
|
||||
};
|
||||
|
||||
if (!ev.recUpdate) { return; }
|
||||
|
||||
var from = ev.recUpdate.from || {};
|
||||
var one = ev.recUpdate.one || {};
|
||||
var s = ev.start;
|
||||
|
||||
// Add "until" date to our recurrenceRule if it has been modified in future occurences
|
||||
var nextRules = getNextRules(ev).filter(function (r) {
|
||||
return r._start > s;
|
||||
});
|
||||
var nextRule = nextRules.shift();
|
||||
|
||||
var applyDiff = function (obj, k) {
|
||||
var diff = obj[k]; // Diff is always compared to origin start/end
|
||||
var d = new Date(ev.raw[k]);
|
||||
d.setDate(d.getDate() + diff.d);
|
||||
d.setHours(d.getHours() + diff.h);
|
||||
d.setMinutes(d.getMinutes() + diff.m);
|
||||
ev[k] = +d;
|
||||
};
|
||||
|
||||
sortUpdate(from).forEach(function (d) {
|
||||
if (s < Number(d)) { return; }
|
||||
Object.keys(from[d]).forEach(function (k) {
|
||||
if (k === 'start' || k === 'end') { return void applyDiff(from[d], k); }
|
||||
if (k === "recurrenceRule" && !from[d][k]) { return; }
|
||||
ev[k] = from[d][k];
|
||||
});
|
||||
});
|
||||
Object.keys(one[s] || {}).forEach(function (k) {
|
||||
if (k === 'start' || k === 'end') { return void applyDiff(one[s], k); }
|
||||
if (k === "recurrenceRule" && !one[s][k]) { return; }
|
||||
ev[k] = one[s][k];
|
||||
});
|
||||
if (ev.deleted) {
|
||||
Object.keys(ev).forEach(function (k) {
|
||||
delete ev[k];
|
||||
});
|
||||
}
|
||||
|
||||
if (nextRule && ev.recurrenceRule) {
|
||||
ev.recurrenceRule._next = nextRule._start - 1;
|
||||
}
|
||||
|
||||
if (ev.reminders) {
|
||||
ev.raw.reminders = ev.reminders;
|
||||
}
|
||||
});
|
||||
return events;
|
||||
};
|
||||
|
||||
|
||||
return Rec;
|
||||
});
|
|
@ -21,10 +21,24 @@ html, body {
|
|||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.summary, .failure, .error, .success {
|
||||
.cp-test-status, .summary {
|
||||
margin-bottom: 1em;
|
||||
border-radius: @corner;
|
||||
padding: 15px;
|
||||
&.success {
|
||||
border: 1px solid green;
|
||||
}
|
||||
&.failure {
|
||||
border: 1px solid red;
|
||||
}
|
||||
&.error {
|
||||
border: 1px solid red;
|
||||
}
|
||||
&.warning {
|
||||
border: 1px solid yellow;
|
||||
}
|
||||
}
|
||||
//.summary, .failure, .error, .success { }
|
||||
|
||||
.pending {
|
||||
border: 1px solid @cryptpad_text_col;
|
||||
|
@ -32,15 +46,6 @@ html, body {
|
|||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
.success {
|
||||
border: 1px solid green;
|
||||
}
|
||||
.failure {
|
||||
border: 1px solid red;
|
||||
}
|
||||
.error {
|
||||
border: 1px solid red;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
@ -84,14 +89,19 @@ html, body {
|
|||
word-break: keep-all;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: @cryptpad_color_link;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-notice-browser, .cp-notice-details, .cp-notice-other {
|
||||
.cp-notice-browser, .cp-notice-details, .cp-notice-other, .cp-notice-customizations {
|
||||
font-size: 70%;
|
||||
}
|
||||
.cp-notice-customizations {
|
||||
border: 1px solid #777;
|
||||
padding: 5px;
|
||||
}
|
||||
.underline { text-decoration: underline; }
|
||||
.cp-app-checkup-version, .cp-app-checkup-browser {
|
||||
.underline;
|
||||
|
|
|
@ -893,6 +893,19 @@ define([
|
|||
});
|
||||
*/
|
||||
|
||||
var parseResponseHeaders = xhr => {
|
||||
var H = {};
|
||||
xhr.getAllResponseHeaders()
|
||||
.split(/\r|\n/)
|
||||
.filter(Boolean)
|
||||
.forEach(line => {
|
||||
line.replace(/([^:]+):(.*)/, (all, key, value) => {
|
||||
H[key] = value.trim();
|
||||
});
|
||||
});
|
||||
return H;
|
||||
};
|
||||
|
||||
var CSP_DESCRIPTIONS = {
|
||||
'default-src': '',
|
||||
'style-src': '',
|
||||
|
@ -1469,9 +1482,110 @@ define([
|
|||
});
|
||||
});
|
||||
|
||||
assert(function (cb, msg) {
|
||||
// public instances are expected to be open for registration
|
||||
// if this is not a public instance, pass this test immediately
|
||||
if (!ApiConfig.listMyInstance) { return cb(true); }
|
||||
// if it's public but registration is not registricted, that's also a pass
|
||||
if (!ApiConfig.restrictRegistration) { return void cb(true); }
|
||||
|
||||
setWarningClass(msg);
|
||||
msg.appendChild(h('span', [
|
||||
"The administrators of this instance have opted in to inclusion in ",
|
||||
link('https://cryptpad.org/instances/', 'the public instance directory'),
|
||||
' but have disabled registration, which is expected to be open.',
|
||||
h('br'),
|
||||
h('br'),
|
||||
" Registration can be reopened using the instance's admin panel.",
|
||||
]));
|
||||
|
||||
cb(false);
|
||||
});
|
||||
|
||||
var compareCustomized = function (a, b, cb) {
|
||||
var getText = (url, done) => {
|
||||
Tools.common_xhr(url, xhr => {
|
||||
xhr.done(done);
|
||||
});
|
||||
};
|
||||
|
||||
var A, B;
|
||||
nThen(w => {
|
||||
getText(a, w(res => {
|
||||
A = res;
|
||||
}));
|
||||
getText(b, w(res => {
|
||||
B = res;
|
||||
}));
|
||||
}).nThen(() => {
|
||||
cb(void 0, A === B);
|
||||
});
|
||||
};
|
||||
|
||||
var CUSTOMIZATIONS = [];
|
||||
// check whether some important pages have been customized
|
||||
assert(function (cb /*, msg */) {
|
||||
nThen(function (w) {
|
||||
// add whatever custom pages you want here
|
||||
[
|
||||
'application_config.js',
|
||||
'pages.js',
|
||||
].forEach(resource => {
|
||||
// sort this above errors and warnings and style in a neutral color.
|
||||
var A = `/customize.dist/${resource}`;
|
||||
var B = `/customize/${resource}`;
|
||||
compareCustomized(A, B, w((err, same) => {
|
||||
if (err || same) { return; }
|
||||
CUSTOMIZATIONS.push(resource);
|
||||
}));
|
||||
});
|
||||
}).nThen(function () {
|
||||
// Implementing these checks as a test was an easy way to ensure that
|
||||
// they completed before the final report was shown. It's intentional
|
||||
// that this always passes
|
||||
cb(true);
|
||||
});
|
||||
});
|
||||
|
||||
var serverToken;
|
||||
assert(function (cb, msg) {
|
||||
Tools.common_xhr('/', function (xhr) {
|
||||
serverToken = xhr.getResponseHeader('server');
|
||||
|
||||
msg.appendChild(h('span', [
|
||||
`Due to its use of `,
|
||||
h('em', `CloudFlare`),
|
||||
` this instance may be inaccessible by users of the Tor network, and generally less secure because of the additional point of failure where code can be intercepted and modified by bad actors.`,
|
||||
]));
|
||||
|
||||
//if (1) { return void cb(false || {serverToken}); }
|
||||
cb(!/cloudflare/i.test(serverToken) || {
|
||||
serverToken,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assert(function (cb, msg) {
|
||||
// provide an exception for development instances
|
||||
if (isLocalhost(trimmedUnsafe) && isLocalhost(window.location.href)) {
|
||||
return void cb(true);
|
||||
}
|
||||
|
||||
msg.appendChild(h('span', [
|
||||
'This instance is not configured to require HTTP Strict Transport Security (HSTS) - which instructs clients to only interact with it over a secure connection.',
|
||||
]));
|
||||
Tools.common_xhr('/', function (xhr) {
|
||||
var H = parseResponseHeaders(xhr);
|
||||
var HSTS = H['strict-transport-security'];
|
||||
|
||||
// check for a numerical value of max-age
|
||||
if (/max\-age=\d+/.test(HSTS)) {
|
||||
return void cb(true);
|
||||
}
|
||||
|
||||
// else call back with the value
|
||||
cb(HSTS);
|
||||
});
|
||||
});
|
||||
|
||||
var row = function (cells) {
|
||||
|
@ -1488,11 +1602,11 @@ define([
|
|||
console.error(err);
|
||||
}
|
||||
|
||||
return h('div.error', [
|
||||
return h(`div.error.cp-test-status.${obj.type}`, [
|
||||
h('h5', obj.message),
|
||||
h('div.table-container',
|
||||
h('table', [
|
||||
row(["Failed test number", obj.test + 1]),
|
||||
row(["Test number", obj.test + 1]),
|
||||
row(["Returned value", h('pre', code(printableValue))]),
|
||||
])
|
||||
),
|
||||
|
@ -1536,21 +1650,82 @@ define([
|
|||
};
|
||||
|
||||
Assert.run(function (state) {
|
||||
var errors = state.errors;
|
||||
var isWarning = function (x) {
|
||||
return x && /cp\-warning/.test(x.getAttribute('class'));
|
||||
};
|
||||
|
||||
var isInfo = x => x && /cp\-info/.test(x.getAttribute('class'));
|
||||
var errors = state.errors; // TODO anomalies might be better?
|
||||
|
||||
var categories = {
|
||||
error: 0,
|
||||
info: 0,
|
||||
warning: 0,
|
||||
};
|
||||
|
||||
errors.forEach(obj => {
|
||||
if (isWarning(obj.message)) {
|
||||
obj.type = 'warning';
|
||||
} else if (isInfo(obj.message)) {
|
||||
obj.type = 'info';
|
||||
state.passed++;
|
||||
} else {
|
||||
obj.type = 'error';
|
||||
}
|
||||
Util.inc(categories, obj.type);
|
||||
});
|
||||
|
||||
var failed = errors.length;
|
||||
|
||||
Messages.assert_numberOfTestsPassed = "{0} / {1} tests passed.";
|
||||
|
||||
var statusClass = failed? 'failure': 'success';
|
||||
var statusClass;
|
||||
if (categories.error !== 0) {
|
||||
statusClass = 'failure';
|
||||
} else if (categories.warning !== 0) {
|
||||
statusClass = 'failure';
|
||||
} else if (categories.info !== 0) {
|
||||
statusClass = 'neutral';
|
||||
} else {
|
||||
statusClass = 'success';
|
||||
}
|
||||
|
||||
var failedDetails = "Details found below";
|
||||
var successDetails = "This checkup only tests the most common configuration issues. You may still experience errors or incorrect behaviour.";
|
||||
var details = h('p.cp-notice-details', failed? failedDetails: successDetails);
|
||||
|
||||
var sortMethod = function (a, b) {
|
||||
if (a.type === 'info' && b.type !== 'info') {
|
||||
return 1;
|
||||
}
|
||||
if (a.type === 'warning' && b.type !== 'warning') {
|
||||
return 1;
|
||||
}
|
||||
return a.test - b.test;
|
||||
};
|
||||
|
||||
var customizations;
|
||||
if (CUSTOMIZATIONS.length) {
|
||||
customizations = h('div.cp-notice-customizations', [
|
||||
h('p', `The following assets have been customized for this instance:`),
|
||||
h('ul', CUSTOMIZATIONS.map(asset => {
|
||||
var href = `/customize/${asset}`;
|
||||
return h('li', [
|
||||
h('a', {
|
||||
href: `href?${+new Date()}`,
|
||||
target: '_blank',
|
||||
}, href),
|
||||
]);
|
||||
})),
|
||||
h('p', `Unexpected behaviour could be related to these changes. If you are this instance's administrator, please try temporarily disabling them before submitting a bug report.`),
|
||||
]);
|
||||
}
|
||||
|
||||
var summary = h('div.summary.' + statusClass, [
|
||||
versionStatement(),
|
||||
serverStatement(serverToken),
|
||||
browserStatement(),
|
||||
customizations,
|
||||
h('p', Messages._getKey('assert_numberOfTestsPassed', [
|
||||
state.passed,
|
||||
state.total
|
||||
|
@ -1558,17 +1733,6 @@ define([
|
|||
details,
|
||||
]);
|
||||
|
||||
var isWarning = function (x) {
|
||||
return x && /cp\-warning/.test(x.getAttribute('class'));
|
||||
};
|
||||
|
||||
var sortMethod = function (a, b) {
|
||||
if (isWarning(a.message) && !isWarning(b.message)) {
|
||||
return 1;
|
||||
}
|
||||
return a.test - b.test;
|
||||
};
|
||||
|
||||
var report = h('div.report', [
|
||||
summary,
|
||||
h('div.failures', errors.sort(sortMethod).map(failureReport)),
|
||||
|
|
|
@ -68,6 +68,7 @@ define([
|
|||
'markdown',
|
||||
'gfm',
|
||||
'html',
|
||||
'asciidoc',
|
||||
'htmlembedded',
|
||||
'htmlmixed',
|
||||
'index.html',
|
||||
|
@ -143,6 +144,31 @@ define([
|
|||
previews['htmlmixed'] = function (val, $div, common) {
|
||||
DiffMd.apply(val, $div, common);
|
||||
};
|
||||
previews['asciidoc'] = function (val, $div, common) {
|
||||
require([
|
||||
'asciidoctor',
|
||||
'/lib/highlight/highlight.pack.js',
|
||||
'css!/lib/highlight/styles/' + (window.CryptPad_theme === 'dark' ? 'dark.css' : 'github.css')
|
||||
], function (asciidoctor) {
|
||||
var reg = asciidoctor.Extensions.create();
|
||||
var Highlight = window.hljs;
|
||||
|
||||
reg.inlineMacro('media-tag', function () {
|
||||
var t = this;
|
||||
t.process(function (parent, target) {
|
||||
var d = target.split('|');
|
||||
return t.createInline(parent, 'quoted', `<media-tag src="${d[0]}" data-crypto-key="${d[1]}"></media-tag>`).convert();
|
||||
});
|
||||
});
|
||||
|
||||
var html = asciidoctor.convert(val, { attributes: 'showtitle', extension_registry: reg });
|
||||
|
||||
DiffMd.apply(html, $div, common);
|
||||
$div.find('pre code').each(function (i, el) {
|
||||
Highlight.highlightBlock(el);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var mkPreviewPane = function (editor, CodeMirror, framework, isPresentMode) {
|
||||
var $previewContainer = $('#cp-app-code-preview');
|
||||
|
@ -370,9 +396,17 @@ define([
|
|||
evModeChange.reg(function (mode) {
|
||||
if (MEDIA_TAG_MODES.indexOf(mode) !== -1) {
|
||||
// Embedding is enabled
|
||||
framework.setMediaTagEmbedder(function (mt) {
|
||||
framework.setMediaTagEmbedder(function (mt, d) {
|
||||
editor.focus();
|
||||
editor.replaceSelection($(mt)[0].outerHTML);
|
||||
var txt = $(mt)[0].outerHTML;
|
||||
if (editor.getMode().name === "asciidoc") {
|
||||
if (d.static) {
|
||||
txt = d.href + `[${d.name}]`;
|
||||
} else {
|
||||
txt = `media-tag:${d.src}|${d.key}[]`;
|
||||
}
|
||||
}
|
||||
editor.replaceSelection(txt);
|
||||
});
|
||||
} else {
|
||||
// Embedding is disabled
|
||||
|
|
|
@ -352,10 +352,21 @@ define([
|
|||
});
|
||||
|
||||
var linkName, linkPassword, linkMessage, linkError, linkSpinText;
|
||||
var linkForm, linkSpin, linkResult;
|
||||
var linkForm, linkSpin, linkResult, linkUses, linkRole;
|
||||
var linkWarning;
|
||||
// Invite from link
|
||||
var dismissButton = h('span.fa.fa-times');
|
||||
|
||||
var roleViewer = UI.createRadio('cp-team-role', 'cp-team-role-viewer',
|
||||
Messages.team_viewers, true, {
|
||||
input: { value: 'VIEWER' },
|
||||
});
|
||||
var roleMember = UI.createRadio('cp-team-role', 'cp-team-role-member',
|
||||
Messages.team_members, false, {
|
||||
input: { value: 'MEMBER' },
|
||||
});
|
||||
|
||||
|
||||
var linkContent = h('div.cp-share-modal', [
|
||||
h('p', Messages.team_inviteLinkTitle ),
|
||||
linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}),
|
||||
|
@ -372,7 +383,7 @@ define([
|
|||
h('div.cp-teams-invite-block', [
|
||||
h('span', Messages.team_inviteLinkSetPassword),
|
||||
h('a.cp-teams-help.fa.fa-question-circle', {
|
||||
href: origin + Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'),
|
||||
href: Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'),
|
||||
target: "_blank",
|
||||
'data-tippy-placement': "right"
|
||||
})
|
||||
|
@ -387,7 +398,21 @@ define([
|
|||
linkMessage = h('textarea.cp-teams-invite-message', {
|
||||
placeholder: Messages.team_inviteLinkNoteMsg,
|
||||
rows: 3
|
||||
})
|
||||
}),
|
||||
linkRole = h('div.cp-teams-invite-block.cp-teams-invite-role',
|
||||
h('span', Messages.team_inviteRole),
|
||||
roleViewer,
|
||||
roleMember
|
||||
),
|
||||
h('div.cp-teams-invite-block.cp-teams-invite-uses',
|
||||
linkUses = h('input', {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 999,
|
||||
value: 1
|
||||
}),
|
||||
h('span', Messages.team_inviteUses)
|
||||
),
|
||||
]),
|
||||
linkSpin = h('div.cp-teams-invite-spinner', {
|
||||
style: 'display: none;'
|
||||
|
@ -407,10 +432,11 @@ define([
|
|||
dismissButton
|
||||
])
|
||||
]);
|
||||
$(linkUses).on('change keyup', function(e) {
|
||||
if (e.target.value === '') { e.target.value = 0; }
|
||||
});
|
||||
$(linkMessage).keydown(function (e) {
|
||||
if (e.which === 13) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
if (e.which === 13) { e.stopPropagation(); }
|
||||
});
|
||||
var localStore = window.cryptpadStore;
|
||||
localStore.get('hide-alert-teamInvite', function (val) {
|
||||
|
@ -428,6 +454,12 @@ define([
|
|||
var $nav = $linkContent.closest('.alertify').find('nav');
|
||||
$(linkError).text('').hide();
|
||||
var name = $(linkName).val();
|
||||
|
||||
var uses = Number($(linkUses).val());
|
||||
if (isNaN(uses) || !uses) { uses = -1; }
|
||||
|
||||
var role = $(linkRole).find("input[name='cp-team-role']:checked").val() || 'VIEWER';
|
||||
|
||||
var pw = $(linkPassword).find('input').val();
|
||||
var msg = $(linkMessage).val();
|
||||
var hash = Hash.createRandomHash('invite', pw);
|
||||
|
@ -461,6 +493,8 @@ define([
|
|||
hash: hash,
|
||||
teamId: config.teamId,
|
||||
seeds: seeds,
|
||||
role: role,
|
||||
uses: uses
|
||||
}, waitFor(function (obj) {
|
||||
if (obj && obj.error) {
|
||||
waitFor.abort();
|
||||
|
@ -1417,7 +1451,7 @@ define([
|
|||
return /HTML/.test(Object.prototype.toString.call(o)) &&
|
||||
typeof(o.tagName) === 'string';
|
||||
};
|
||||
var allowedTags = ['a', 'p', 'hr', 'div'];
|
||||
var allowedTags = ['a', 'li', 'p', 'hr', 'div'];
|
||||
var isValidOption = function (o) {
|
||||
if (typeof o !== "object") { return false; }
|
||||
if (isElement(o)) { return true; }
|
||||
|
@ -1541,6 +1575,7 @@ define([
|
|||
$innerblock.find('.cp-dropdown-element-active').removeClass('cp-dropdown-element-active');
|
||||
if (config.isSelect && value) {
|
||||
// We use JSON.stringify here to escape quotes
|
||||
if (typeof(value) === "object") { value = JSON.stringify(value); }
|
||||
var $val = $innerblock.find('[data-value='+JSON.stringify(value)+']');
|
||||
setActive($val);
|
||||
try {
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
};
|
||||
|
||||
Util.clone = function (o) {
|
||||
if (o === undefined || o === null) { return o; }
|
||||
return JSON.parse(JSON.stringify(o));
|
||||
};
|
||||
|
||||
|
|
|
@ -125,6 +125,12 @@ define([
|
|||
formSeed = obj;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
if (!formSeed) { // no drive mode
|
||||
formSeed = localStorage.CP_formSeed || Hash.createChannelId();
|
||||
localStorage.CP_formSeed = formSeed;
|
||||
} else {
|
||||
delete localStorage.CP_formSeed;
|
||||
}
|
||||
cb({
|
||||
curvePrivate: curvePrivate,
|
||||
curvePublic: curvePrivate && Hash.getCurvePublicFromPrivate(curvePrivate),
|
||||
|
@ -135,28 +141,162 @@ define([
|
|||
common.getFormAnswer = function (data, cb) {
|
||||
postMessage("GET", {
|
||||
key: ['forms', data.channel],
|
||||
}, cb);
|
||||
};
|
||||
common.storeFormAnswer = function (data) {
|
||||
postMessage("SET", {
|
||||
}, function (obj) {
|
||||
if (obj && obj.error === "ENODRIVE") {
|
||||
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
|
||||
return void cb(all[data.channel]);
|
||||
}
|
||||
if (obj && obj.error) { return void cb(obj); }
|
||||
|
||||
if (obj) {
|
||||
if (!Array.isArray(obj)) { obj = [obj]; }
|
||||
return void cb(obj);
|
||||
}
|
||||
|
||||
// We have a drive and no answer but maybe we had
|
||||
// previous "nodrive" answers: migrate
|
||||
var old = Util.tryParse(localStorage.CP_formAnswers || "{}");
|
||||
if (Array.isArray(old[data.channel])) {
|
||||
var d = old[data.channel];
|
||||
return void postMessage("SET", {
|
||||
key: ['forms', data.channel],
|
||||
value: {
|
||||
value: d
|
||||
}, function (obj) {
|
||||
// Delete old data if it was correctly stored in the drive
|
||||
if (obj && obj.error) { return void cb(d); }
|
||||
delete old[data.channel];
|
||||
localStorage.CP_formAnswers = JSON.stringify(old);
|
||||
cb(d);
|
||||
});
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
};
|
||||
common.storeFormAnswer = function (data, cb) {
|
||||
var answer = {
|
||||
uid: data.uid,
|
||||
hash: data.hash,
|
||||
curvePrivate: data.curvePrivate,
|
||||
anonymous: data.anonymous
|
||||
}
|
||||
};
|
||||
var answers = [];
|
||||
Nthen(function (waitFor) {
|
||||
common.getFormAnswer(data, waitFor(function (obj) {
|
||||
if (!obj || obj.error) { return; }
|
||||
answers = obj;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
answers.push(answer);
|
||||
postMessage("SET", {
|
||||
key: ['forms', data.channel],
|
||||
value: answers
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) {
|
||||
if (obj.error === "ENODRIVE") {
|
||||
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
|
||||
all[data.channel] = answers;
|
||||
localStorage.CP_formAnswers = JSON.stringify(all);
|
||||
/*
|
||||
|
||||
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
|
||||
if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); }
|
||||
localStorage.CP_formAnswered = JSON.stringify(answered);
|
||||
return;
|
||||
*/
|
||||
return void cb();
|
||||
}
|
||||
console.error(obj.error);
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
};
|
||||
common.deleteFormAnswers = function (data, _cb) {
|
||||
var cb = Util.once(_cb);
|
||||
common.getFormAnswer(data, function (obj) {
|
||||
if (!obj || obj.error) { return void cb(); }
|
||||
if (!obj.length) { return void cb(); }
|
||||
var n = Nthen;
|
||||
var nacl, theirs;
|
||||
n = n(function (waitFor) {
|
||||
require(['/bower_components/tweetnacl/nacl-fast.min.js'], waitFor(function () {
|
||||
nacl = window.nacl;
|
||||
var s = new Uint8Array(32);
|
||||
theirs = nacl.box.keyPair.fromSecretKey(s);
|
||||
}));
|
||||
}).nThen;
|
||||
var toDelete = [];
|
||||
obj.forEach(function (answer) {
|
||||
if (answer.uid !== data.uid) { return; }
|
||||
n = n(function (waitFor) {
|
||||
var hash = answer.hash;
|
||||
var h = nacl.util.decodeUTF8(hash);
|
||||
|
||||
// Make proof
|
||||
var curve = answer.curvePrivate;
|
||||
var mySecret = nacl.util.decodeBase64(curve);
|
||||
var nonce = nacl.randomBytes(24);
|
||||
var proofBytes = nacl.box(h, nonce, theirs.publicKey, mySecret);
|
||||
var proof = nacl.util.encodeBase64(nonce) +'|'+ nacl.util.encodeBase64(proofBytes);
|
||||
var lineData = {
|
||||
channel: data.channel,
|
||||
hash: hash,
|
||||
proof: proof
|
||||
};
|
||||
postMessage("DELETE_MAILBOX_MESSAGE", lineData, waitFor(function (obj) {
|
||||
if (obj && obj.error && obj.error !== 'HASH_NOT_FOUND') {
|
||||
// If HASH_NOT_FOUND, the message is already deleted
|
||||
// so we can delete it locally
|
||||
waitFor.abort();
|
||||
return void cb(obj);
|
||||
}
|
||||
toDelete.push(hash);
|
||||
}));
|
||||
}).nThen;
|
||||
});
|
||||
n(function () {
|
||||
obj = obj.filter(function (answer) { return !toDelete.includes(answer.hash); });
|
||||
if (!obj.length) { obj = undefined; }
|
||||
postMessage("SET", {
|
||||
key: ['forms', data.channel],
|
||||
value: obj
|
||||
}, function (_obj) {
|
||||
if (_obj && _obj.error === "ENODRIVE") {
|
||||
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
|
||||
if (obj) { all[data.channel] = obj; }
|
||||
else { delete all[data.channel]; }
|
||||
localStorage.CP_formAnswers = JSON.stringify(all);
|
||||
return void cb();
|
||||
}
|
||||
return void cb(_obj);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
common.muteChannel = function (channel, state, cb) {
|
||||
var mutedChannels = [];
|
||||
Nthen(function (waitFor) {
|
||||
postMessage("GET", {
|
||||
key: ['mutedChannels'],
|
||||
}, waitFor(function (obj) {
|
||||
if (obj && obj.error) { waitFor.abort(); return void cb(obj); }
|
||||
mutedChannels = obj || [];
|
||||
}));
|
||||
}).nThen(function () {
|
||||
if (state) {
|
||||
if (!mutedChannels.includes(channel)) {
|
||||
mutedChannels.push(channel);
|
||||
}
|
||||
} else {
|
||||
mutedChannels = mutedChannels.filter(function (chan) {
|
||||
return chan !== channel;
|
||||
});
|
||||
}
|
||||
postMessage("SET", {
|
||||
key: ['mutedChannels'],
|
||||
value: mutedChannels
|
||||
}, cb);
|
||||
});
|
||||
};
|
||||
|
||||
common.makeNetwork = function (cb) {
|
||||
|
@ -431,8 +571,8 @@ define([
|
|||
}, todo);
|
||||
};
|
||||
|
||||
common.clearOwnedChannel = function (channel, cb) {
|
||||
postMessage("CLEAR_OWNED_CHANNEL", channel, cb);
|
||||
common.clearOwnedChannel = function (data, cb) {
|
||||
postMessage("CLEAR_OWNED_CHANNEL", data, cb);
|
||||
};
|
||||
// "force" allows you to delete your drive ID
|
||||
common.removeOwnedChannel = function (data, cb) {
|
||||
|
@ -1154,8 +1294,8 @@ define([
|
|||
pad.onMetadataEvent = Util.mkEvent();
|
||||
pad.onChannelDeleted = Util.mkEvent();
|
||||
|
||||
pad.requestAccess = function (data, cb) {
|
||||
postMessage("REQUEST_PAD_ACCESS", data, cb);
|
||||
pad.contactOwner = function (data, cb) {
|
||||
postMessage("CONTACT_PAD_OWNER", data, cb);
|
||||
};
|
||||
pad.giveAccess = function (data, cb) {
|
||||
postMessage("GIVE_PAD_ACCESS", data, cb);
|
||||
|
@ -2385,6 +2525,7 @@ define([
|
|||
anonHash: LocalStore.getFSHash(),
|
||||
localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), // TODO move this to LocalStore ?
|
||||
language: common.getLanguage(),
|
||||
form_seed: localStorage.CP_formSeed,
|
||||
cache: rdyCfg.cache,
|
||||
noDrive: rdyCfg.noDrive,
|
||||
disableCache: localStorage['CRYPTPAD_STORE|disableCache'],
|
||||
|
|
|
@ -78,6 +78,7 @@ define([
|
|||
var TAGS_NAME = Messages.fm_tagsName;
|
||||
var SHARED_FOLDER = 'sf';
|
||||
var SHARED_FOLDER_NAME = Messages.fm_sharedFolderName;
|
||||
var FILTER = "filter";
|
||||
|
||||
// Icons
|
||||
var faFolder = 'cptools-folder';
|
||||
|
@ -1149,8 +1150,12 @@ define([
|
|||
common.getMediaTagPreview(mts, idx);
|
||||
};
|
||||
|
||||
var refresh = APP.refresh = function () {
|
||||
APP.displayDirectory(currentPath);
|
||||
var FILTER_BY = "filterBy";
|
||||
|
||||
var refresh = APP.refresh = function (cb) {
|
||||
var type = APP.store[FILTER_BY];
|
||||
var path = type ? [FILTER, type, currentPath] : currentPath;
|
||||
APP.displayDirectory(path, undefined, cb);
|
||||
};
|
||||
|
||||
// `app`: true (force open wiht the app), false (force open in preview),
|
||||
|
@ -2940,72 +2945,147 @@ define([
|
|||
$block.find('a.cp-app-drive-new-link, li.cp-app-drive-new-link').click(showLinkModal);
|
||||
}
|
||||
$block.find('a.cp-app-drive-new-doc, li.cp-app-drive-new-doc')
|
||||
.click(function () {
|
||||
.on('click auxclick', function (e) {
|
||||
e.preventDefault();
|
||||
var type = $(this).attr('data-type') || 'pad';
|
||||
var path = manager.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
|
||||
openIn(type, path, APP.team);
|
||||
});
|
||||
};
|
||||
var getNewPadOptions = function (isInRoot) {
|
||||
var options = [];
|
||||
if (isInRoot) {
|
||||
options.push({
|
||||
class: 'cp-app-drive-new-folder',
|
||||
icon: $folderIcon.clone()[0],
|
||||
name: Messages.fm_folder,
|
||||
});
|
||||
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
|
||||
options.push({
|
||||
class: 'cp-app-drive-new-shared-folder',
|
||||
icon: $sharedFolderIcon.clone()[0],
|
||||
name: Messages.fm_sharedFolder,
|
||||
});
|
||||
}
|
||||
options.push({ separator: true });
|
||||
options.push({
|
||||
class: 'cp-app-drive-new-fileupload',
|
||||
icon: getIcon('fileupload')[0],
|
||||
name: Messages.uploadButton,
|
||||
});
|
||||
if (APP.allowFolderUpload) {
|
||||
options.push({
|
||||
class: 'cp-app-drive-new-folderupload',
|
||||
icon: getIcon('folderupload')[0],
|
||||
name: Messages.uploadFolderButton,
|
||||
});
|
||||
}
|
||||
options.push({ separator: true });
|
||||
options.push({
|
||||
class: 'cp-app-drive-new-link',
|
||||
icon: getIcon('link')[0],
|
||||
name: Messages.fm_link_new,
|
||||
});
|
||||
options.push({ separator: true });
|
||||
}
|
||||
getNewPadTypes().forEach(function (type) {
|
||||
var typeClass = 'cp-app-drive-new-doc';
|
||||
|
||||
var premium = common.checkRestrictedApp(type);
|
||||
if (premium < 0) {
|
||||
typeClass += ' cp-app-hidden cp-app-disabled';
|
||||
} else if (premium === 0) {
|
||||
typeClass += ' cp-app-disabled';
|
||||
}
|
||||
|
||||
options.push({
|
||||
class: typeClass,
|
||||
type: type,
|
||||
icon: getIcon(type)[0],
|
||||
name: Messages.type[type],
|
||||
});
|
||||
});
|
||||
|
||||
if (APP.store[FILTER_BY]) {
|
||||
var typeFilter = APP.store[FILTER_BY];
|
||||
options = options.filter((obj) => {
|
||||
if (obj.separator) { return false; }
|
||||
|
||||
if (typeFilter === 'link') {
|
||||
return obj.class.includes('cp-app-drive-new-link');
|
||||
}
|
||||
if (typeFilter === 'file') {
|
||||
return obj.class.includes('cp-app-drive-new-fileupload');
|
||||
}
|
||||
if (getNewPadTypes().indexOf(typeFilter) !== -1) {
|
||||
return typeFilter === obj.type;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
var createNewButton = function (isInRoot, $container) {
|
||||
if (!APP.editable) { return; }
|
||||
if (!APP.loggedIn) { return; } // Anonymous users can use the + menu in the toolbar
|
||||
|
||||
if (!manager.isPathIn(currentPath, [ROOT, 'hrefArray'])) { return; }
|
||||
|
||||
// Create dropdown
|
||||
var options = getNewPadOptions(isInRoot).map(function (obj) {
|
||||
if (obj.separator) { return { tag: 'hr' }; }
|
||||
var newObj = {
|
||||
tag: 'a',
|
||||
attributes: { 'class': obj.class },
|
||||
content: [ obj.icon, obj.name ]
|
||||
};
|
||||
if (obj.type) {
|
||||
newObj.attributes['data-type'] = obj.type;
|
||||
newObj.attributes['href'] = APP.origin + Hash.hashToHref('', obj.type);
|
||||
}
|
||||
return newObj;
|
||||
});
|
||||
var dropdownConfig = {
|
||||
buttonContent: [
|
||||
h('i.fa.fa-plus'),
|
||||
h('span.cp-button-name', Messages.fm_newButton),
|
||||
],
|
||||
buttonCls: 'cp-toolbar-dropdown-nowrap',
|
||||
options: options,
|
||||
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
|
||||
common: common
|
||||
};
|
||||
var $block = UIElements.createDropdown(dropdownConfig);
|
||||
|
||||
// Custom style:
|
||||
$block.find('button').addClass('cp-app-drive-toolbar-new');
|
||||
|
||||
addNewPadHandlers($block, isInRoot);
|
||||
|
||||
$container.append($block);
|
||||
};
|
||||
|
||||
var createFilterButton = function (isTemplate, $container) {
|
||||
if (!APP.loggedIn) { return; }
|
||||
|
||||
// Create dropdown
|
||||
var options = [];
|
||||
if (isInRoot) {
|
||||
if (APP.store[FILTER_BY]) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-app-drive-new-folder pewpew'},
|
||||
attributes: {
|
||||
'class': 'cp-app-drive-rm-filter',
|
||||
},
|
||||
content: [
|
||||
$folderIcon.clone()[0],
|
||||
Messages.fm_folder,
|
||||
],
|
||||
});
|
||||
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-app-drive-new-shared-folder'},
|
||||
content: [
|
||||
$sharedFolderIcon.clone()[0],
|
||||
Messages.fm_sharedFolder,
|
||||
],
|
||||
});
|
||||
}
|
||||
options.push({tag: 'hr'});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-app-drive-new-fileupload'},
|
||||
content: [
|
||||
getIcon('fileupload')[0],
|
||||
Messages.uploadButton,
|
||||
],
|
||||
});
|
||||
if (APP.allowFolderUpload) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-app-drive-new-folderupload'},
|
||||
content: [
|
||||
getIcon('folderupload')[0],
|
||||
Messages.uploadFolderButton,
|
||||
],
|
||||
});
|
||||
}
|
||||
options.push({tag: 'hr'});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-app-drive-new-link'},
|
||||
content: [
|
||||
getIcon('link')[0],
|
||||
Messages.fm_link_new,
|
||||
h('i.fa.fa-times'),
|
||||
Messages.fm_rmFilter,
|
||||
],
|
||||
});
|
||||
options.push({tag: 'hr'});
|
||||
}
|
||||
getNewPadTypes().forEach(function (type) {
|
||||
var attributes = {
|
||||
'class': 'cp-app-drive-new-doc',
|
||||
'class': 'cp-app-drive-filter-doc',
|
||||
'data-type': type,
|
||||
'href': '#'
|
||||
};
|
||||
|
@ -3026,21 +3106,72 @@ define([
|
|||
],
|
||||
});
|
||||
});
|
||||
if (!isTemplate) {
|
||||
options.push({tag: 'hr'});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {
|
||||
'class': 'cp-app-drive-filter-doc',
|
||||
'data-type': 'link'
|
||||
},
|
||||
content: [
|
||||
getIcon('link')[0],
|
||||
Messages.fm_link_type,
|
||||
],
|
||||
});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {
|
||||
'class': 'cp-app-drive-filter-doc',
|
||||
'data-type': 'file',
|
||||
'href': '#'
|
||||
},
|
||||
content: [
|
||||
getIcon('file')[0],
|
||||
Messages.type['file'],
|
||||
],
|
||||
});
|
||||
}
|
||||
var dropdownConfig = {
|
||||
buttonContent: [
|
||||
h('span.fa.fa-plus'),
|
||||
h('span', Messages.fm_newButton),
|
||||
h('i.fa.fa-filter'),
|
||||
h('span.cp-button-name', Messages.fm_filterBy),
|
||||
],
|
||||
buttonCls: 'cp-toolbar-dropdown-nowrap',
|
||||
options: options,
|
||||
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
|
||||
feedback: 'DRIVE_FILTERBY',
|
||||
common: common
|
||||
};
|
||||
if (APP.store[FILTER_BY]) {
|
||||
var type = APP.store[FILTER_BY];
|
||||
var message = type === 'link' ? Messages.fm_link_type : Messages.type[type];
|
||||
dropdownConfig.buttonContent.push(
|
||||
h('span.cp-button-name', ':'),
|
||||
getIcon(type)[0],
|
||||
h('span.cp-button-name', message)
|
||||
);
|
||||
}
|
||||
var $block = UIElements.createDropdown(dropdownConfig);
|
||||
|
||||
// Custom style:
|
||||
$block.find('button').addClass('cp-app-drive-toolbar-new');
|
||||
// Add style
|
||||
if (APP.store[FILTER_BY]) {
|
||||
$block.find('button').addClass('cp-toolbar-button-active');
|
||||
}
|
||||
|
||||
addNewPadHandlers($block, isInRoot);
|
||||
// Add handlers
|
||||
if (APP.store[FILTER_BY]) {
|
||||
$block.find('a.cp-app-drive-rm-filter')
|
||||
.click(function () {
|
||||
APP.store[FILTER_BY] = undefined;
|
||||
APP.displayDirectory(currentPath);
|
||||
});
|
||||
}
|
||||
$block.find('a.cp-app-drive-filter-doc')
|
||||
.click(function () {
|
||||
var type = $(this).attr('data-type') || 'invalid-filter';
|
||||
APP.store[FILTER_BY] = type;
|
||||
APP.displayDirectory([FILTER, type, currentPath]);
|
||||
});
|
||||
|
||||
$container.append($block);
|
||||
};
|
||||
|
@ -3302,65 +3433,38 @@ define([
|
|||
return keys;
|
||||
};
|
||||
|
||||
var filterPads = function (files, type, path, useId) {
|
||||
var root = path && manager.find(path);
|
||||
|
||||
return files
|
||||
.filter(function (e) {
|
||||
return useId ? manager.isFile(e) : (path && manager.isFile(root[e]));
|
||||
})
|
||||
.filter(function (e) {
|
||||
var id = useId ? e : root[e];
|
||||
var data = manager.getFileData(id);
|
||||
if (type === 'link') { return data.static; }
|
||||
var href = data.href || data.roHref;
|
||||
return href ? (href.split('/')[1] === type) : true;
|
||||
// if types are unreachable, display files to avoid misleading the user
|
||||
});
|
||||
};
|
||||
|
||||
// Create the ghost icon to add pads/folders
|
||||
var createNewPadIcons = function ($block, isInRoot) {
|
||||
var $container = $('<div>');
|
||||
if (isInRoot) {
|
||||
// Folder
|
||||
var $element1 = $('<li>', {
|
||||
'class': 'cp-app-drive-new-folder cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend($folderIcon.clone()).appendTo($container);
|
||||
$element1.append($('<span>', { 'class': 'cp-app-drive-new-name' })
|
||||
.text(Messages.fm_folder));
|
||||
// Shared Folder
|
||||
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
|
||||
var $element3 = $('<li>', {
|
||||
'class': 'cp-app-drive-new-shared-folder cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend($sharedFolderIcon.clone()).appendTo($container);
|
||||
$element3.append($('<span>', { 'class': 'cp-app-drive-new-name' })
|
||||
.text(Messages.fm_sharedFolder));
|
||||
}
|
||||
// Upload file
|
||||
var $elementFileUpload = $('<li>', {
|
||||
'class': 'cp-app-drive-new-fileupload cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend(getIcon('fileupload')).appendTo($container);
|
||||
$elementFileUpload.append($('<span>', {'class': 'cp-app-drive-new-name'})
|
||||
.text(Messages.uploadButton));
|
||||
// Upload folder
|
||||
if (APP.allowFolderUpload) {
|
||||
var $elementFolderUpload = $('<li>', {
|
||||
'class': 'cp-app-drive-new-folderupload cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend(getIcon('folderupload')).appendTo($container);
|
||||
$elementFolderUpload.append($('<span>', {'class': 'cp-app-drive-new-name'})
|
||||
.text(Messages.uploadFolderButton));
|
||||
}
|
||||
// Link
|
||||
var $elementLink = $('<li>', {
|
||||
'class': 'cp-app-drive-new-link cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend(getIcon('link')).appendTo($container);
|
||||
$elementLink.append($('<span>', {'class': 'cp-app-drive-new-name'})
|
||||
.text(Messages.fm_link_type));
|
||||
}
|
||||
// Pads
|
||||
getNewPadTypes().forEach(function (type) {
|
||||
var $element = $('<li>', {
|
||||
'class': 'cp-app-drive-new-doc cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend(getIcon(type)).appendTo($container);
|
||||
$element.append($('<span>', {'class': 'cp-app-drive-new-name'})
|
||||
.text(Messages.type[type]));
|
||||
$element.attr('data-type', type);
|
||||
getNewPadOptions(isInRoot).forEach(function (obj) {
|
||||
if (obj.separator) { return; }
|
||||
|
||||
var premium = common.checkRestrictedApp(type);
|
||||
if (premium < 0) {
|
||||
$element.addClass('cp-app-hidden cp-app-disabled');
|
||||
} else if (premium === 0) {
|
||||
$element.addClass('cp-app-disabled');
|
||||
var $element = $('<li>', {
|
||||
'class': obj.class + ' cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend(obj.icon).appendTo($container);
|
||||
$element.append($('<span>', { 'class': 'cp-app-drive-new-name' })
|
||||
.text(obj.name));
|
||||
|
||||
if (obj.type) {
|
||||
$element.attr('data-type', obj.type);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -3435,7 +3539,7 @@ define([
|
|||
|
||||
// Unsorted element are represented by "href" in an array: they don't have a filename
|
||||
// and they don't hav a hierarchical structure (folder/subfolders)
|
||||
var displayHrefArray = function ($container, rootName, draggable) {
|
||||
var displayHrefArray = function ($container, rootName, draggable, typeFilter) {
|
||||
var unsorted = files[rootName];
|
||||
if (unsorted.length) {
|
||||
var $fileHeader = getFileListHeader(true);
|
||||
|
@ -3445,6 +3549,7 @@ define([
|
|||
var sortBy = APP.store[SORT_FILE_BY];
|
||||
sortBy = sortBy === "" ? sortBy = 'name' : sortBy;
|
||||
var sortedFiles = sortElements(false, [rootName], keys, sortBy, !getSortFileDesc(), true);
|
||||
sortedFiles = typeFilter ? filterPads(sortedFiles, typeFilter, false, true) : sortedFiles;
|
||||
sortedFiles.forEach(function (id) {
|
||||
var file = manager.getFileData(id);
|
||||
if (!file) {
|
||||
|
@ -3526,7 +3631,7 @@ define([
|
|||
createGhostIcon($container);
|
||||
};
|
||||
|
||||
var displayTrashRoot = function ($list, $folderHeader, $fileHeader) {
|
||||
var displayTrashRoot = function ($list, $folderHeader, $fileHeader, typeFilter) {
|
||||
var filesList = [];
|
||||
var root = files[TRASH];
|
||||
var isEmpty = true;
|
||||
|
@ -3549,14 +3654,25 @@ define([
|
|||
isEmpty = false;
|
||||
});
|
||||
|
||||
var sortedFolders = typeFilter ? [] : sortTrashElements(true, filesList, null, !getSortFolderDesc());
|
||||
var sortedFiles = sortTrashElements(false, filesList, APP.store[SORT_FILE_BY], !getSortFileDesc);
|
||||
|
||||
if (typeFilter) {
|
||||
var ids = sortedFiles.map(function (obj) { return obj.element; });
|
||||
var idsFilter = filterPads(ids, typeFilter, false, true);
|
||||
sortedFiles = sortedFiles.filter(function (obj) {
|
||||
return (idsFilter.indexOf(obj.element) !== -1);
|
||||
});
|
||||
// prevent trash emptying while filter is active
|
||||
isEmpty = true;
|
||||
}
|
||||
|
||||
if (!isEmpty) {
|
||||
var $empty = createEmptyTrashButton();
|
||||
$content.append($empty);
|
||||
}
|
||||
|
||||
var sortedFolders = sortTrashElements(true, filesList, null, !getSortFolderDesc());
|
||||
var sortedFiles = sortTrashElements(false, filesList, APP.store[SORT_FILE_BY], !getSortFileDesc());
|
||||
if (manager.hasSubfolder(root, true)) { $list.append($folderHeader); }
|
||||
if (!typeFilter && manager.hasSubfolder(root, true)) { $list.append($folderHeader); }
|
||||
sortedFolders.forEach(function (f) {
|
||||
var $element = createElement([TRASH], f.spath, root, true);
|
||||
$list.append($element);
|
||||
|
@ -3728,7 +3844,7 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
var displayRecent = function ($list) {
|
||||
var displayRecent = function ($list, typeFilter) {
|
||||
var filesList = manager.getRecentPads();
|
||||
var limit = 20;
|
||||
|
||||
|
@ -3744,6 +3860,14 @@ define([
|
|||
var i = 0;
|
||||
var channels = [];
|
||||
|
||||
if (typeFilter) {
|
||||
var ids = filesList.map(function (arr) { return arr[0]; });
|
||||
var idsFilter = filterPads(ids, typeFilter, false, true);
|
||||
filesList = filesList.filter(function (arr) {
|
||||
return (idsFilter.indexOf(arr[0]) !== -1);
|
||||
});
|
||||
}
|
||||
|
||||
$list.append(h('li.cp-app-drive-element-separator', h('span', Messages.drive_active1Day)));
|
||||
filesList.some(function (arr) {
|
||||
var id = arr[0];
|
||||
|
@ -3932,6 +4056,17 @@ define([
|
|||
$content.html("");
|
||||
sel.$selectBox = $('<div>', {'class': 'cp-app-drive-content-select-box'})
|
||||
.appendTo($content);
|
||||
|
||||
var typeFilter;
|
||||
var isFilter = path[0] === FILTER;
|
||||
if (isFilter) {
|
||||
if (path.length < 3) { return; }
|
||||
typeFilter = path[1];
|
||||
path = path[2];
|
||||
currentPath = path;
|
||||
} else {
|
||||
APP.store[FILTER_BY] = undefined;
|
||||
}
|
||||
var isInRoot = manager.isPathIn(path, [ROOT]);
|
||||
var inTrash = manager.isPathIn(path, [TRASH]);
|
||||
var isTrashRoot = manager.comparePath(path, [TRASH]);
|
||||
|
@ -3939,6 +4074,8 @@ define([
|
|||
var isAllFiles = manager.comparePath(path, [FILES_DATA]);
|
||||
var isVirtual = virtualCategories.indexOf(path[0]) !== -1;
|
||||
var isSearch = path[0] === SEARCH;
|
||||
var isRecent = path[0] === RECENT;
|
||||
var isOwned = path[0] === OWNED;
|
||||
var isTags = path[0] === TAGS;
|
||||
// ANON_SHARED_FOLDER
|
||||
var isSharedFolder = path[0] === SHARED_FOLDER && APP.newSharedFolder;
|
||||
|
@ -4040,6 +4177,9 @@ define([
|
|||
if (!readOnlyFolder) {
|
||||
createNewButton(isInRoot, APP.toolbar.$bottomL);
|
||||
}
|
||||
if (!isTags && !isSearch) {
|
||||
createFilterButton(isTemplate, APP.toolbar.$bottomL);
|
||||
}
|
||||
|
||||
if (APP.mobile()) {
|
||||
var $context = $('<button>', {
|
||||
|
@ -4075,16 +4215,16 @@ define([
|
|||
var $fileHeader = getFileListHeader(true);
|
||||
|
||||
if (isTemplate) {
|
||||
displayHrefArray($list, path[0], true);
|
||||
displayHrefArray($list, path[0], true, typeFilter);
|
||||
} else if (isAllFiles) {
|
||||
displayAllFiles($list);
|
||||
} else if (isTrashRoot) {
|
||||
displayTrashRoot($list, $folderHeader, $fileHeader);
|
||||
displayTrashRoot($list, $folderHeader, $fileHeader, typeFilter);
|
||||
} else if (isSearch) {
|
||||
displaySearch($list, path[1]);
|
||||
} else if (path[0] === RECENT) {
|
||||
displayRecent($list);
|
||||
} else if (path[0] === OWNED) {
|
||||
} else if (isRecent) {
|
||||
displayRecent($list, typeFilter);
|
||||
} else if (isOwned) {
|
||||
displayOwned($list);
|
||||
} else if (isTags) {
|
||||
displayTags($list);
|
||||
|
@ -4093,11 +4233,12 @@ define([
|
|||
displaySharedFolder($list);
|
||||
} else {
|
||||
if (!inTrash) { $dirContent.contextmenu(openContextMenu('content')); }
|
||||
if (manager.hasSubfolder(root)) { $list.append($folderHeader); }
|
||||
if (!isFilter && manager.hasSubfolder(root)) { $list.append($folderHeader); }
|
||||
// display sub directories
|
||||
var keys = Object.keys(root);
|
||||
var sortedFolders = sortElements(true, path, keys, null, !getSortFolderDesc());
|
||||
var sortedFolders = isFilter ? [] : sortElements(true, path, keys, null, !getSortFolderDesc());
|
||||
var sortedFiles = sortElements(false, path, keys, APP.store[SORT_FILE_BY], !getSortFileDesc());
|
||||
sortedFiles = isFilter ? filterPads(sortedFiles, typeFilter, path) : sortedFiles;
|
||||
sortedFolders.forEach(function (key) {
|
||||
if (manager.isFile(root[key])) { return; }
|
||||
var $element = createElement(path, key, root, true);
|
||||
|
@ -4149,10 +4290,12 @@ define([
|
|||
|
||||
appStatus.ready(true);
|
||||
};
|
||||
var displayDirectory = APP.displayDirectory = function (path, force) {
|
||||
var displayDirectory = APP.displayDirectory = function (path, force, cb) {
|
||||
cb = cb || function () {};
|
||||
if (APP.closed || (APP.$content && !$.contains(document.documentElement, APP.$content[0]))) { return; }
|
||||
if (history.isHistoryMode) {
|
||||
return void _displayDirectory(path, force);
|
||||
_displayDirectory(path, force);
|
||||
return void cb();
|
||||
}
|
||||
if (!manager.comparePath(currentPath, path)) {
|
||||
removeSelected();
|
||||
|
@ -4161,6 +4304,7 @@ define([
|
|||
copyObjectValue(files, proxy.drive);
|
||||
updateSharedFolders(sframeChan, manager, files, folders, function () {
|
||||
_displayDirectory(path, force);
|
||||
cb();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -5206,8 +5350,9 @@ define([
|
|||
};
|
||||
APP.FM = common.createFileManager(fmConfig);
|
||||
|
||||
refresh();
|
||||
refresh(function () {
|
||||
UI.removeLoadingScreen();
|
||||
});
|
||||
|
||||
/*
|
||||
if (!APP.team) {
|
||||
|
|
|
@ -102,6 +102,7 @@ define([
|
|||
// Send the command
|
||||
sframeChan.query('Q_SET_PAD_METADATA', {
|
||||
channel: channel,
|
||||
channels: otherChan,
|
||||
command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS',
|
||||
value: [ed],
|
||||
teamId: teamOwner
|
||||
|
@ -335,11 +336,6 @@ define([
|
|||
}
|
||||
}).nThen(function (waitFor) {
|
||||
var href = data.href;
|
||||
var hashes = priv.hashes || {};
|
||||
var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash;
|
||||
if (data.fakeHref) {
|
||||
href = Hash.hashToHref(bestHash, priv.app);
|
||||
}
|
||||
sel.forEach(function (el) {
|
||||
var curve = $(el).attr('data-curve');
|
||||
if (curve === user.curvePublic) { return; }
|
||||
|
@ -928,12 +924,7 @@ define([
|
|||
}
|
||||
|
||||
var href = data.href;
|
||||
var hashes = priv.hashes || {};
|
||||
var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash;
|
||||
if (data.fakeHref) {
|
||||
href = Hash.hashToHref(bestHash, priv.app);
|
||||
}
|
||||
var isNotStored = Boolean(data.fakeHref);
|
||||
var isNotStored = Boolean(data.isNotStored);
|
||||
sframeChan.query(q, {
|
||||
teamId: typeof(owned) !== "boolean" ? owned : undefined,
|
||||
href: href,
|
||||
|
@ -1055,13 +1046,13 @@ define([
|
|||
var owned = Modal.isOwned(Env, data);
|
||||
|
||||
// Request edit access
|
||||
if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned && !opts.calendar && priv.app !== 'form') {
|
||||
if (common.isLoggedIn() && data.roHref && !owned && !opts.calendar && priv.app !== 'form') {
|
||||
var requestButton = h('button.btn.btn-secondary.no-margin.cp-access-margin-right',
|
||||
Messages.requestEdit_button);
|
||||
var requestBlock = h('p', requestButton);
|
||||
var $requestBlock = $(requestBlock).hide();
|
||||
content.push(requestBlock);
|
||||
sframeChan.query('Q_REQUEST_ACCESS', {
|
||||
sframeChan.query('Q_CONTACT_OWNER', {
|
||||
send: false,
|
||||
metadata: data
|
||||
}, function (err, obj) {
|
||||
|
@ -1072,9 +1063,10 @@ define([
|
|||
$requestBlock.show().find('button').click(function () {
|
||||
if (spinner.getState()) { return; }
|
||||
spinner.spin();
|
||||
sframeChan.query('Q_REQUEST_ACCESS', {
|
||||
sframeChan.query('Q_CONTACT_OWNER', {
|
||||
send: true,
|
||||
metadata: data
|
||||
metadata: data,
|
||||
query: "REQUEST_PAD_ACCESS"
|
||||
}, function (err, obj) {
|
||||
if (obj && obj.state) {
|
||||
UI.log(Messages.requestEdit_sent);
|
||||
|
|
|
@ -42,11 +42,13 @@ define([
|
|||
if (err || !val) {
|
||||
if (opts.access) {
|
||||
data.password = priv.password;
|
||||
// Access modal and the pad is not stored: we're not an owner
|
||||
// so we don't need the correct href, just the type
|
||||
var h = Hash.createRandomHash(priv.app, priv.password);
|
||||
data.fakeHref = true;
|
||||
data.href = base + priv.pathname + '#' + h;
|
||||
// Access modal and the pad is not stored: get the hashes from outer
|
||||
var hashes = priv.hashes || {};
|
||||
data.href = Hash.hashToHref(hashes.editHash || hashes.fileHash, priv.app);
|
||||
if (hashes.viewHash) {
|
||||
data.roHref = Hash.hashToHref(hashes.viewHash, priv.app);
|
||||
}
|
||||
data.isNotStored = true;
|
||||
} else {
|
||||
waitFor.abort();
|
||||
return void cb(err || 'EEMPTY');
|
||||
|
|
|
@ -37,7 +37,6 @@ define([
|
|||
}));
|
||||
}
|
||||
|
||||
if (!data.fakeHref) {
|
||||
if (data.href) {
|
||||
$('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d);
|
||||
$d.append(UI.dialog.selectable(data.href, {
|
||||
|
@ -51,7 +50,6 @@ define([
|
|||
id: 'cp-app-prop-rolink',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (data.tags && Array.isArray(data.tags)) {
|
||||
$d.append(h('div.cp-app-prop', [Messages.fm_prop_tagsList, h('br'), h('span.cp-app-prop-content', data.tags.join(', '))]));
|
||||
|
|
|
@ -590,8 +590,9 @@ define([
|
|||
if (!channel.isFriendChat) { return; }
|
||||
var curvePublic = channel.curvePublic;
|
||||
var friend = contactsData[curvePublic] || friendData;
|
||||
var name = Util.fixHTML(UI.getDisplayName(friend.name || friend.displayName));
|
||||
var content = h('div', [
|
||||
UI.setHTML(h('p'), Messages._getKey('contacts_confirmRemove', [Util.fixHTML(friend.name)])),
|
||||
UI.setHTML(h('p'), Messages._getKey('contacts_confirmRemove', [ name ])),
|
||||
]);
|
||||
UI.confirm(content, function (yes) {
|
||||
if (!yes) { return; }
|
||||
|
@ -709,6 +710,7 @@ define([
|
|||
var curvePublic = info.curvePublic;
|
||||
contactsData[curvePublic] = info;
|
||||
|
||||
if (!Array.isArray(types)) { return; }
|
||||
if (types.indexOf('displayName') !== -1) {
|
||||
var name = info.displayName;
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ define([
|
|||
"Asterisk asterisk",
|
||||
"Brainfuck brainfuck .b",
|
||||
"C text/x-csrc .c",
|
||||
"C text/x-c++src .cpp",
|
||||
"C++ text/x-c++src .cpp",
|
||||
"C-like clike .c",
|
||||
"Clojure clojure .clj",
|
||||
"CMake cmake _", /* no extension */
|
||||
|
@ -50,7 +50,6 @@ define([
|
|||
"HTML htmlmixed .html",
|
||||
"HTTP http _", /* no extension */
|
||||
"IDL idl .idl",
|
||||
"JADE jade .jade",
|
||||
"Java text/x-java .java",
|
||||
"JavaScript javascript .js",
|
||||
"Jinja2 jinja2 .j2",
|
||||
|
|
|
@ -335,6 +335,28 @@ define([
|
|||
}
|
||||
};
|
||||
|
||||
handlers['FORM_RESPONSE'] = function(common, data) {
|
||||
var content = data.content;
|
||||
var msg = content.msg;
|
||||
|
||||
// Display the notification
|
||||
var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
|
||||
var href = msg.content.href;
|
||||
|
||||
content.getFormatText = function() {
|
||||
return Messages._getKey('form_responseNotification', [title]);
|
||||
};
|
||||
if (href) {
|
||||
content.handler = function() {
|
||||
common.openURL(href);
|
||||
defaultDismiss(common, data)();
|
||||
};
|
||||
}
|
||||
if (!content.archived) {
|
||||
content.dismissHandler = defaultDismiss(common, data);
|
||||
}
|
||||
};
|
||||
|
||||
handlers['COMMENT_REPLY'] = function(common, data) {
|
||||
var content = data.content;
|
||||
var msg = content.msg;
|
||||
|
@ -481,6 +503,18 @@ define([
|
|||
var missed = content.msg.missed;
|
||||
var start = msg.start;
|
||||
var title = Util.fixHTML(msg.title);
|
||||
content.handler = function () {
|
||||
var priv = common.getMetadataMgr().getPrivateData();
|
||||
var time = Util.find(data, ['content', 'msg', 'content', 'start']);
|
||||
if (priv.app === "calendar" && window.APP && window.APP.moveToDate) {
|
||||
return void window.APP.moveToDate(time);
|
||||
}
|
||||
var url = Hash.hashToHref('', 'calendar');
|
||||
var optsUrl = Hash.getNewPadURL(url, {
|
||||
time: time
|
||||
});
|
||||
common.openURL(optsUrl);
|
||||
};
|
||||
content.getFormatText = function () {
|
||||
var now = +new Date();
|
||||
|
||||
|
|
|
@ -342,10 +342,11 @@ define([
|
|||
cb(account);
|
||||
};
|
||||
|
||||
// clearOwnedChannel is only used for private chat at the moment
|
||||
// clearOwnedChannel is only used for private chat and forms
|
||||
Store.clearOwnedChannel = function (clientId, data, cb) {
|
||||
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
|
||||
store.rpc.clearOwnedChannel(data, function (err) {
|
||||
var s = getStore(data && data.teamId);
|
||||
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
|
||||
s.rpc.clearOwnedChannel(data.channel, function (err) {
|
||||
cb({error:err});
|
||||
});
|
||||
};
|
||||
|
@ -659,6 +660,7 @@ define([
|
|||
offline: store.proxy && store.offline,
|
||||
teams: teams,
|
||||
plan: store.ready ? (account.plan || '') : undefined,
|
||||
mutedChannels: proxy.mutedChannels
|
||||
}
|
||||
};
|
||||
cb(JSON.parse(JSON.stringify(metadata)));
|
||||
|
@ -1947,42 +1949,23 @@ define([
|
|||
}).nThen(cb);
|
||||
};
|
||||
|
||||
// requestPadAccess is used to check if we have a way to contact the owner
|
||||
// of the pad AND to send the request if we want
|
||||
// data.send === false ==> check if we can contact them
|
||||
// data.send === true ==> send the request
|
||||
Store.requestPadAccess = function (clientId, data, cb) {
|
||||
// contactPadOwner is used to send "REQUEST_ACCESS" messages
|
||||
// and to notify form owners when sending a response
|
||||
Store.contactPadOwner = function (clientId, data, cb) {
|
||||
var owner = data.owner;
|
||||
|
||||
// If the owner was not is the pad metadata, check if it is a friend.
|
||||
// We'll contact the first owner for whom we know the mailbox
|
||||
/* // TODO decide whether we want to re-enable this feature for our own contacts
|
||||
// communicate the exception to users that 'muting' won't apply to friends
|
||||
check mailbox in our contacts is not compatible with the new "mute pad" feature
|
||||
var owners = data.owners;
|
||||
if (!owner && Array.isArray(owners)) {
|
||||
var friends = store.proxy.friends || {};
|
||||
// If we have friends, check if an owner is one of them (with a mailbox)
|
||||
if (Object.keys(friends).filter(function (curve) { return curve !== 'me'; }).length) {
|
||||
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 send is true, send the request to the owner.
|
||||
if (owner) {
|
||||
if (data.send) {
|
||||
store.mailbox.sendTo('REQUEST_PAD_ACCESS', {
|
||||
channel: data.channel
|
||||
var sendTo = function (query, msg, user, _cb) {
|
||||
if (store.mailbox && !data.anon) {
|
||||
return store.mailbox.sendTo(query, msg, user, _cb);
|
||||
}
|
||||
Mailbox.sendToAnon(store.anon_rpc, query, msg, user, _cb);
|
||||
};
|
||||
sendTo(data.query, {
|
||||
channel: data.channel,
|
||||
data: data.msgData
|
||||
}, {
|
||||
channel: owner.notifications,
|
||||
curvePublic: owner.curvePublic
|
||||
|
@ -2179,6 +2162,13 @@ define([
|
|||
}
|
||||
};
|
||||
|
||||
Store.deleteMailboxMessage = function (clientId, data, cb) {
|
||||
if (!store.anon_rpc) { return void cb({error: 'RPC_NOT_READY'}); }
|
||||
store.anon_rpc.send('DELETE_MAILBOX_MESSAGE', data, function (e) {
|
||||
cb({error:e});
|
||||
});
|
||||
};
|
||||
|
||||
// GET_FULL_HISTORY from sframe-common-outer
|
||||
Store.getFullHistory = function (clientId, data, cb) {
|
||||
var network = store.network;
|
||||
|
@ -2450,6 +2440,27 @@ define([
|
|||
// Clients management
|
||||
var driveEventClients = [];
|
||||
|
||||
// Check if this is a channel that we shouldn't leave when closing the debug app
|
||||
var alwaysOnline = function (chanId) {
|
||||
if (!store) { return; }
|
||||
// Drive
|
||||
if (store.driveChannel === chanId) { return true; }
|
||||
// Shared folders
|
||||
if (SF.isSharedFolderChannel(chanId)) { return true; }
|
||||
// Teams
|
||||
if (Util.find(store, ['proxy', 'teams'])) {
|
||||
var t = Util.find(store, ['proxy', 'teams']) || {};
|
||||
return Object.keys(t).some(function (id) {
|
||||
return t[id].channel === chanId;
|
||||
});
|
||||
}
|
||||
// Profile
|
||||
if (Util.find(store, ['proxy', 'profile', 'href'])) {
|
||||
return Hash.hrefToHexChannelId(Util.find(store, ['proxy', 'profile', 'href']))
|
||||
=== chanId;
|
||||
}
|
||||
};
|
||||
|
||||
var dropChannel = Store.dropChannel = function (chanId) {
|
||||
console.error('Drop channel', chanId);
|
||||
|
||||
|
@ -2462,6 +2473,14 @@ define([
|
|||
try {
|
||||
store.onlyoffice.leavePad(chanId);
|
||||
} catch (e) { console.error(e); }
|
||||
|
||||
try {
|
||||
if (alwaysOnline(chanId)) {
|
||||
delete Store.channels[chanId];
|
||||
return;
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
|
||||
try {
|
||||
Cache.leaveChannel(chanId);
|
||||
} catch (e) { console.error(e); }
|
||||
|
@ -2973,6 +2992,9 @@ define([
|
|||
if (!rt.proxy.uid && store.noDriveUid) {
|
||||
rt.proxy.uid = store.noDriveUid;
|
||||
}
|
||||
if (!rt.proxy.form_seed && data.form_seed) {
|
||||
rt.proxy.form_seed = data.form_seed;
|
||||
}
|
||||
/*
|
||||
// deprecating localStorage migration as of 4.2.0
|
||||
var drive = rt.proxy.drive;
|
||||
|
|
|
@ -4,12 +4,13 @@ define([
|
|||
'/common/common-constants.js',
|
||||
'/common/common-realtime.js',
|
||||
'/common/outer/cache-store.js',
|
||||
'/calendar/recurrence.js',
|
||||
'/customize/messages.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
'chainpad-listmap',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) {
|
||||
], function (Util, Hash, Constants, Realtime, Cache, Rec, Messages, nThen, Listmap, Crypto, ChainPad) {
|
||||
var Calendar = {};
|
||||
|
||||
var getStore = function (ctx, id) {
|
||||
|
@ -90,7 +91,29 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
var updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
|
||||
var getRecurring = function (ev) {
|
||||
var mid = new Date();
|
||||
var start = new Date(mid.getFullYear(), mid.getMonth()-1, 15);
|
||||
var end = new Date(mid.getFullYear(), mid.getMonth()+1, 15);
|
||||
var startId = Rec.getMonthId(start);
|
||||
var midId = Rec.getMonthId(mid);
|
||||
var endId = Rec.getMonthId(end);
|
||||
|
||||
var toAdd = Rec.getRecurring([startId, midId, endId], [ev]);
|
||||
|
||||
var all = [ev];
|
||||
Array.prototype.push.apply(all, toAdd);
|
||||
return Rec.applyUpdates(all);
|
||||
};
|
||||
var clearDismissed = function (ctx, uid) {
|
||||
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
|
||||
Object.keys(h).filter(function (id) {
|
||||
return id.indexOf(uid) === 0;
|
||||
}).forEach(function (id) {
|
||||
delete h[id];
|
||||
});
|
||||
};
|
||||
var _updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
|
||||
var now = +new Date();
|
||||
var ev = Util.clone(_ev);
|
||||
var uid = ev.id;
|
||||
|
@ -101,6 +124,10 @@ define([
|
|||
}
|
||||
reminders[uid] = [];
|
||||
|
||||
if (_ev.deleted) { return; }
|
||||
|
||||
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]) || []; // dismissed
|
||||
|
||||
var last = ctx.store.data.lastVisit;
|
||||
|
||||
if (ev.isAllDay) {
|
||||
|
@ -119,10 +146,11 @@ define([
|
|||
if (ev.end <= now && !missed) {
|
||||
// No reminder for past events
|
||||
delete reminders[uid];
|
||||
clearDismissed(ctx, uid);
|
||||
return;
|
||||
}
|
||||
|
||||
var send = function () {
|
||||
var send = function (d) {
|
||||
var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']);
|
||||
if (hide) { return; }
|
||||
var ctime = ev.start <= now ? ev.start : +new Date(); // Correct order for past events
|
||||
|
@ -133,11 +161,18 @@ define([
|
|||
missed: Boolean(missed),
|
||||
content: ev
|
||||
},
|
||||
hash: 'REMINDER|'+uid
|
||||
hash: 'REMINDER|'+uid+'-'+d
|
||||
}, null, function () {
|
||||
});
|
||||
};
|
||||
var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); };
|
||||
var sent = false;
|
||||
var sendNotif = function (delay) {
|
||||
sent = true;
|
||||
|
||||
ctx.Store.onReadyEvt.reg(function () {
|
||||
send(delay);
|
||||
});
|
||||
};
|
||||
|
||||
var notifs = ev.reminders || [];
|
||||
notifs.sort(function (a, b) {
|
||||
|
@ -148,6 +183,10 @@ define([
|
|||
var delay = delayMinutes * 60000;
|
||||
var time = now + delay;
|
||||
|
||||
if (d.some(function (minutes) {
|
||||
return delayMinutes >= minutes;
|
||||
})) { return; }
|
||||
|
||||
// setTimeout only work with 32bit timeout values. If the event is too far away,
|
||||
// ignore this event for now
|
||||
// FIXME: call this function again in xxx days to reload these missing timeout?
|
||||
|
@ -156,18 +195,35 @@ define([
|
|||
// If we're too late to send a notification, send it instantly and ignore
|
||||
// all notifications that were supposed to be sent even earlier
|
||||
if (ev.start <= time) {
|
||||
sendNotif();
|
||||
sendNotif(delayMinutes);
|
||||
return true;
|
||||
}
|
||||
|
||||
// It starts in more than "delay": prepare the notification
|
||||
reminders[uid].push(setTimeout(function () {
|
||||
sendNotif();
|
||||
sendNotif(delayMinutes);
|
||||
}, (ev.start - time)));
|
||||
});
|
||||
|
||||
if (!sent) {
|
||||
// Remone any existing notification from the UI
|
||||
ctx.Store.onReadyEvt.reg(function () {
|
||||
ctx.store.mailbox.hideMessage('reminders', {
|
||||
hash: 'REMINDER|'+uid
|
||||
}, null, function () {
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
var updateEventReminders = function (ctx, reminders, ev, useLastVisit) {
|
||||
var all = getRecurring(Util.clone(ev));
|
||||
all.forEach(function (_ev) {
|
||||
_updateEventReminders(ctx, reminders, _ev, useLastVisit);
|
||||
});
|
||||
};
|
||||
var addReminders = function (ctx, id, ev) {
|
||||
var calendar = ctx.calendars[id];
|
||||
if (!ev) { return; } // XXX deleted event remote: delete reminders
|
||||
if (!calendar || !calendar.reminders) { return; }
|
||||
if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
|
||||
|
||||
|
@ -352,10 +408,20 @@ define([
|
|||
c.lm = lm;
|
||||
var proxy = c.proxy = lm.proxy;
|
||||
|
||||
var _updateCalled = false;
|
||||
var _update = function () {
|
||||
if (_updateCalled) { return; }
|
||||
_updateCalled = true;
|
||||
setTimeout(function () {
|
||||
_updateCalled = false;
|
||||
update();
|
||||
});
|
||||
};
|
||||
|
||||
lm.proxy.on('cacheready', function () {
|
||||
if (!proxy.metadata) { return; }
|
||||
c.cacheready = true;
|
||||
setTimeout(update);
|
||||
_update();
|
||||
if (cb) { cb(null, lm.proxy); }
|
||||
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
|
||||
}).on('ready', function (info) {
|
||||
|
@ -372,24 +438,39 @@ define([
|
|||
title: data.title
|
||||
};
|
||||
}
|
||||
setTimeout(update);
|
||||
_update();
|
||||
if (cb) { cb(null, lm.proxy); }
|
||||
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
|
||||
}).on('change', [], function () {
|
||||
if (!c.ready) { return; }
|
||||
setTimeout(update);
|
||||
_update();
|
||||
}).on('change', ['content'], function (o, n, p) {
|
||||
if (p.length === 2 && n && !o) { // New event
|
||||
addReminders(ctx, channel, n);
|
||||
return void addReminders(ctx, channel, n);
|
||||
}
|
||||
if (p.length === 2 && !n && o) { // Deleted event
|
||||
addReminders(ctx, channel, {
|
||||
return void addReminders(ctx, channel, {
|
||||
id: p[1],
|
||||
start: 0
|
||||
});
|
||||
}
|
||||
if (p.length === 3 && n && o && p[2] === 'start') { // Update event start
|
||||
setTimeout(function () {
|
||||
if (p.length >= 3 && ['start','reminders','isAllDay'].includes(p[2])) {
|
||||
// Updated event
|
||||
return void setTimeout(function () {
|
||||
addReminders(ctx, channel, proxy.content[p[1]]);
|
||||
});
|
||||
}
|
||||
if (p.length >= 6 && ['start','reminders','isAllDay'].includes(p[5])) {
|
||||
// Updated recurring event
|
||||
return void setTimeout(function () {
|
||||
addReminders(ctx, channel, proxy.content[p[1]]);
|
||||
});
|
||||
}
|
||||
}).on('remove', ['content'], function (x, p) {
|
||||
_update();
|
||||
if ((p.length >= 3 && p[2] === 'reminders') ||
|
||||
(p.length >= 6 && p[5] === 'reminders')) {
|
||||
return void setTimeout(function () {
|
||||
addReminders(ctx, channel, proxy.content[p[1]]);
|
||||
});
|
||||
}
|
||||
|
@ -400,10 +481,10 @@ define([
|
|||
updateLocalCalendars(ctx, c, md);
|
||||
}).on('disconnect', function () {
|
||||
c.offline = true;
|
||||
setTimeout(update);
|
||||
_update();
|
||||
}).on('reconnect', function () {
|
||||
c.offline = false;
|
||||
setTimeout(update);
|
||||
_update();
|
||||
}).on('error', function (info) {
|
||||
if (!info || !info.error) { return; }
|
||||
if (info.error === "EDELETED" ) {
|
||||
|
@ -411,7 +492,7 @@ define([
|
|||
}
|
||||
if (info.error === "ERESTRICTED" ) {
|
||||
c.restricted = true;
|
||||
setTimeout(update);
|
||||
_update();
|
||||
}
|
||||
cb(info);
|
||||
});
|
||||
|
@ -760,8 +841,11 @@ define([
|
|||
var ev = c.proxy.content[data.ev.id];
|
||||
if (!ev) { return void cb({error: "EINVAL"}); }
|
||||
|
||||
data.rawData = data.rawData || {};
|
||||
|
||||
// update the event
|
||||
var changes = data.changes || {};
|
||||
var type = data.type || {};
|
||||
|
||||
var newC;
|
||||
if (changes.calendarId) {
|
||||
|
@ -770,7 +854,122 @@ define([
|
|||
newC.proxy.content = newC.proxy.content || {};
|
||||
}
|
||||
|
||||
var RECUPDATE = {
|
||||
one: {},
|
||||
from: {}
|
||||
};
|
||||
if (['one','from','all'].includes(type.which)) {
|
||||
ev.recUpdate = ev.recUpdate || RECUPDATE;
|
||||
if (!ev.recUpdate.one) { ev.recUpdate.one = {}; }
|
||||
if (!ev.recUpdate.from) { ev.recUpdate.from = {}; }
|
||||
}
|
||||
var update = ev.recUpdate;
|
||||
var alwaysAll = ['calendarId'];
|
||||
var keys = Object.keys(changes).filter(function (s) {
|
||||
// we can only change the calendar or recurrence rule on the origin
|
||||
return !alwaysAll.includes(s);
|
||||
});
|
||||
|
||||
// Delete (future) affected keys
|
||||
var cleanAfter = function (time) {
|
||||
[update.from, update.one].forEach(function (obj) {
|
||||
Object.keys(obj).forEach(function (d) {
|
||||
if (Number(d) < time) { return; }
|
||||
delete obj[d];
|
||||
});
|
||||
});
|
||||
};
|
||||
var cleanKeys = function (obj, when) {
|
||||
Object.keys(obj).forEach(function (d) {
|
||||
if (when && Number(d) < when) { return; }
|
||||
keys.forEach(function (k) {
|
||||
delete obj[d][k];
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Update recurrence rule. We may create a new event here
|
||||
var dontSendUpdate = false;
|
||||
if (typeof(changes.recurrenceRule) !== "undefined") {
|
||||
if (['one','from'].includes(type.which) && !data.rawData.isOrigin) {
|
||||
cleanAfter(type.when);
|
||||
} else {
|
||||
update = ev.recUpdate = RECUPDATE;
|
||||
}
|
||||
}
|
||||
|
||||
if (type.which === "one") {
|
||||
update.one[type.when] = update.one[type.when] || {};
|
||||
// Nothing to delete
|
||||
} else if (type.which === "from") {
|
||||
update.from[type.when] = update.from[type.when] || {};
|
||||
// Delete all "single/from" updates (affected keys only) after this "from" date
|
||||
cleanKeys(update.from, type.when);
|
||||
cleanKeys(update.one, type.when);
|
||||
} else if (type.which === "all") {
|
||||
// Delete all "single/from" updates (affected keys only) after
|
||||
cleanKeys(update.from);
|
||||
cleanKeys(update.one);
|
||||
}
|
||||
|
||||
if (changes.start && update && (!type.which || type.which === "all")) {
|
||||
var diff = changes.start - ev.start;
|
||||
var newOne = {};
|
||||
var newFrom = {};
|
||||
Object.keys(update.one || {}).forEach(function (time) {
|
||||
newOne[Number(time)+diff] = update.one[time];
|
||||
});
|
||||
Object.keys(update.from || {}).forEach(function (time) {
|
||||
newFrom[Number(time)+diff] = update.from[time];
|
||||
});
|
||||
update.one = newOne;
|
||||
update.from = newFrom;
|
||||
}
|
||||
|
||||
|
||||
// Clear the "dismissed" reminders when the user is updating reminders
|
||||
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
|
||||
if (changes.reminders) {
|
||||
if (type.which === 'one') {
|
||||
if (!type.when || type.when === ev.start) { delete h[data.ev.id]; }
|
||||
else { delete h[data.ev.id +'|'+ type.when]; }
|
||||
} else if (type.which === "from") {
|
||||
Object.keys(h).filter(function (id) {
|
||||
return id.indexOf(data.ev.id) === 0;
|
||||
}).forEach(function (id) {
|
||||
var time = Number(id.split('|')[1]);
|
||||
if (!time) { return; }
|
||||
if (time < type.when) { return; }
|
||||
delete h[id];
|
||||
});
|
||||
} else {
|
||||
Object.keys(h).filter(function (id) {
|
||||
return id.indexOf(data.ev.id) === 0;
|
||||
}).forEach(function (id) {
|
||||
delete h[id];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the changes
|
||||
Object.keys(changes).forEach(function (key) {
|
||||
if (!alwaysAll.includes(key) && type.which === "one") {
|
||||
if (key === "recurrenceRule") {
|
||||
if (data.rawData && data.rawData.isOrigin) {
|
||||
return (ev[key] = changes[key]);
|
||||
}
|
||||
// Always "from", never "one" for recurrence rules
|
||||
update.from[type.when] = update.from[type.when] || {};
|
||||
return (update.from[type.when][key] = changes[key]);
|
||||
}
|
||||
update.one[type.when][key] = changes[key];
|
||||
return;
|
||||
}
|
||||
if (!alwaysAll.includes(key) && type.which === "from") {
|
||||
update.from[type.when][key] = changes[key];
|
||||
return;
|
||||
}
|
||||
ev[key] = changes[key];
|
||||
});
|
||||
|
||||
|
@ -790,6 +989,7 @@ define([
|
|||
delete c.proxy.content[data.ev.id];
|
||||
}
|
||||
|
||||
|
||||
nThen(function (waitFor) {
|
||||
Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor());
|
||||
if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); }
|
||||
|
@ -806,8 +1006,8 @@ define([
|
|||
addReminders(ctx, id, ev);
|
||||
}
|
||||
|
||||
sendUpdate(ctx, c);
|
||||
if (newC) { sendUpdate(ctx, newC); }
|
||||
if (!dontSendUpdate || newC) { sendUpdate(ctx, c); }
|
||||
if (newC && !dontSendUpdate) { sendUpdate(ctx, newC); }
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
@ -816,7 +1016,22 @@ define([
|
|||
var c = ctx.calendars[id];
|
||||
if (!c) { return void cb({error: "ENOENT"}); }
|
||||
c.proxy.content = c.proxy.content || {};
|
||||
var evId = data.id.split('|')[0];
|
||||
if (data.id === evId) {
|
||||
delete c.proxy.content[data.id];
|
||||
} else {
|
||||
var ev = c.proxy.content[evId];
|
||||
var s = data.raw && data.raw.start;
|
||||
if (s) {
|
||||
ev.recUpdate = ev.recUpdate || {
|
||||
one: {},
|
||||
from: {}
|
||||
};
|
||||
ev.recUpdate.one[s] = {
|
||||
deleted: true
|
||||
};
|
||||
}
|
||||
}
|
||||
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
|
||||
addReminders(ctx, id, {
|
||||
id: data.id,
|
||||
|
@ -867,6 +1082,20 @@ define([
|
|||
openChannels(ctx);
|
||||
}));
|
||||
|
||||
ctx.store.proxy.on('change', ['hideReminders'], function (o,n,p) {
|
||||
var uid = p[1].split('|')[0];
|
||||
Object.keys(ctx.calendars).some(function (calId) {
|
||||
var c = ctx.calendars[calId];
|
||||
if (!c || !c.proxy || !c.proxy.content) { return; }
|
||||
if (c.proxy.content[uid]) {
|
||||
setTimeout(function () {
|
||||
addReminders(ctx, calId, c.proxy.content[uid]);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
calendar.closeTeam = function (teamId) {
|
||||
Object.keys(ctx.calendars).forEach(function (id) {
|
||||
var ctxCal = ctx.calendars[id];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
(function () {
|
||||
var factory = function (Util, Cred, Nacl) {
|
||||
var factory = function (Util, Cred, Nacl, Crypto) {
|
||||
var Invite = {};
|
||||
|
||||
var encode64 = Nacl.util.encodeBase64;
|
||||
|
@ -49,6 +49,24 @@ var factory = function (Util, Cred, Nacl) {
|
|||
roster.invite(toInvite, cb);
|
||||
};
|
||||
|
||||
// Invite links should only be visible to members or above, so
|
||||
// we store them in the roster encrypted with a string only available
|
||||
// to users with edit rights
|
||||
var decodeUTF8 = Nacl.util.decodeUTF8;
|
||||
Invite.encryptHash = function (data, seedStr) {
|
||||
var array = decodeUTF8(seedStr);
|
||||
var bytes = Nacl.hash(array);
|
||||
var cryptKey = bytes.subarray(0, 32);
|
||||
return Crypto.encrypt(data, cryptKey);
|
||||
};
|
||||
Invite.decryptHash = function (encryptedStr, seedStr) {
|
||||
var array = decodeUTF8(seedStr);
|
||||
var bytes = Nacl.hash(array);
|
||||
var cryptKey = bytes.subarray(0, 32);
|
||||
return Crypto.decrypt(encryptedStr, cryptKey);
|
||||
};
|
||||
|
||||
|
||||
/* INPUTS
|
||||
|
||||
* password (for scrypt)
|
||||
|
@ -84,16 +102,17 @@ var factory = function (Util, Cred, Nacl) {
|
|||
module.exports = factory(
|
||||
require("../common-util"),
|
||||
require("../common-credential.js"),
|
||||
require("nthen"),
|
||||
require("tweetnacl/nacl-fast")
|
||||
require("tweetnacl/nacl-fast"),
|
||||
require("chainpad-crypto/crypto")
|
||||
);
|
||||
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
|
||||
define([
|
||||
'/common/common-util.js',
|
||||
'/common/common-credential.js',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
], function (Util, Cred) {
|
||||
return factory(Util, Cred, window.nacl);
|
||||
], function (Util, Cred, Crypto) {
|
||||
return factory(Util, Cred, window.nacl, Crypto);
|
||||
});
|
||||
}
|
||||
}());
|
||||
|
|
|
@ -20,6 +20,10 @@ define([
|
|||
if (!curvePublic) { return false; }
|
||||
return Boolean(muted[curvePublic]);
|
||||
};
|
||||
var isChannelMuted = function (ctx, channel) {
|
||||
var muted = ctx.store.proxy.mutedChannels || [];
|
||||
return muted.includes(channel);
|
||||
};
|
||||
|
||||
// Store the friend request displayed to avoid duplicates
|
||||
var friendRequest = {};
|
||||
|
@ -575,6 +579,61 @@ define([
|
|||
cb();
|
||||
};
|
||||
|
||||
// Hide duplicates when receiving a form notification:
|
||||
// Keep only one notification per channel
|
||||
var formNotifs = {};
|
||||
handlers['FORM_RESPONSE'] = function (ctx, box, data, cb) {
|
||||
var msg = data.msg;
|
||||
var hash = data.hash;
|
||||
var content = msg.content;
|
||||
|
||||
var channel = content.channel;
|
||||
if (!channel) { return void cb(true); }
|
||||
|
||||
if (isChannelMuted(ctx, channel)) { return void cb(true); }
|
||||
|
||||
var title, href;
|
||||
ctx.Store.getAllStores().some(function (s) {
|
||||
var res = s.manager.findChannel(channel);
|
||||
// Check if the pad is in our drive
|
||||
return res.some(function (obj) {
|
||||
if (!obj.data) { return; }
|
||||
if (href && !obj.data.href) { return; } // We already have the VIEW url, we need EDIT
|
||||
href = obj.data.href || obj.data.roHref;
|
||||
title = obj.data.filename || obj.data.title;
|
||||
if (obj.data.href) { return true; } // Abort only if we have the EDIT url
|
||||
});
|
||||
});
|
||||
|
||||
// If we don't have the edit url, ignore this notification
|
||||
if (!href) { return void cb(true); }
|
||||
|
||||
// Add the title
|
||||
content.href = href;
|
||||
content.title = title;
|
||||
|
||||
// Remove duplicates
|
||||
var old = formNotifs[channel];
|
||||
var toRemove = old ? old.data : undefined;
|
||||
|
||||
// Update the data
|
||||
formNotifs[channel] = {
|
||||
data: {
|
||||
type: box.type,
|
||||
hash: hash
|
||||
}
|
||||
};
|
||||
|
||||
cb(false, toRemove);
|
||||
};
|
||||
removeHandlers['FORM_RESPONSE'] = function (ctx, box, data, hash) {
|
||||
var content = data.content;
|
||||
var channel = content.channel;
|
||||
var old = formNotifs[channel];
|
||||
if (old && old.data && old.data.hash === hash) {
|
||||
delete formNotifs[channel];
|
||||
}
|
||||
};
|
||||
// Hide duplicates when receiving a SHARE_PAD notification:
|
||||
// Keep only one notification per channel: the stronger and more recent one
|
||||
var comments = {};
|
||||
|
|
|
@ -164,6 +164,22 @@ proxy.mailboxes = {
|
|||
});
|
||||
});
|
||||
};
|
||||
Mailbox.sendToAnon = function (anonRpc, type, msg, user, cb) {
|
||||
var Nacl = Crypto.Nacl;
|
||||
var curveSeed = Nacl.randomBytes(32);
|
||||
var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
|
||||
var curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
|
||||
var curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
|
||||
sendTo({
|
||||
store: {
|
||||
anon_rpc: anonRpc,
|
||||
proxy: {
|
||||
curvePrivate: curvePrivate,
|
||||
curvePublic: curvePublic
|
||||
}
|
||||
}
|
||||
}, type, msg, user, cb);
|
||||
};
|
||||
|
||||
// Mark a message as read
|
||||
var dismiss = function (ctx, data, cId, cb) {
|
||||
|
@ -177,6 +193,15 @@ proxy.mailboxes = {
|
|||
hideMessage(ctx, type, hash, ctx.clients.filter(function (clientId) {
|
||||
return clientId !== cId;
|
||||
}));
|
||||
|
||||
var uid = hash.slice(9).split('-')[0];
|
||||
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]);
|
||||
if (!d) {
|
||||
var h = ctx.store.proxy.hideReminders = ctx.store.proxy.hideReminders || {};
|
||||
d = h[uid] = h[uid] || [];
|
||||
}
|
||||
var delay = hash.split('-')[1];
|
||||
if (delay && !d.includes(delay)) { d.push(Number(delay)); }
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -590,6 +615,9 @@ proxy.mailboxes = {
|
|||
});
|
||||
};
|
||||
|
||||
mailbox.hideMessage = function (type, msg) {
|
||||
hideMessage(ctx, type, msg.hash, ctx.clients);
|
||||
};
|
||||
mailbox.showMessage = function (type, msg, cId, cb) {
|
||||
if (type === "reminders" && msg) {
|
||||
ctx.boxes.reminders.content[msg.hash] = msg.msg;
|
||||
|
|
|
@ -238,6 +238,8 @@ define([
|
|||
var removeFromFriendList = function (ctx, curvePublic, cb) {
|
||||
var proxy = ctx.store.proxy;
|
||||
var friends = proxy.friends;
|
||||
// FIXME this probably shouldn't happen, but functions that take callbacks
|
||||
// should be guaranteed to call back.
|
||||
if (!friends) { return; }
|
||||
delete friends[curvePublic];
|
||||
Realtime.whenRealtimeSyncs(ctx.store.realtime, function () {
|
||||
|
@ -447,7 +449,12 @@ define([
|
|||
var msg = [Types.unfriend, proxy.curvePublic, +new Date()];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encrypt(msgStr);
|
||||
channel.wc.bcast(cryptMsg).then(function () {}, function (err) {
|
||||
channel.wc.bcast(cryptMsg).then(function () {
|
||||
onFriendRemoved(ctx, curvePublic, data.channel);
|
||||
removeFromFriendList(ctx, curvePublic, function () {
|
||||
cb();
|
||||
});
|
||||
}, function (err) {
|
||||
if (err) { return void cb({error:err}); }
|
||||
onFriendRemoved(ctx, curvePublic, data.channel);
|
||||
removeFromFriendList(ctx, curvePublic, function () {
|
||||
|
|
|
@ -463,9 +463,22 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback)
|
|||
if (typeof(members[curve]) !== 'undefined') { throw new Error("MEMBER_ALREADY_PRESENT"); }
|
||||
|
||||
// copy the new profile from the old one
|
||||
members[curve] = Util.clone(members[author]);
|
||||
// and erase the old one
|
||||
var clone = Util.clone(members[author]);
|
||||
delete clone.remaining;
|
||||
delete clone.totalUses;
|
||||
delete clone.inviteChannel;
|
||||
delete clone.previewChannel;
|
||||
members[curve] = clone;
|
||||
|
||||
// XXX
|
||||
var remaining = members[author].remaining || 1;
|
||||
if (remaining === -1) { return true; } // Infinite uses, keep the link
|
||||
if (remaining > 1) { // Remove 1 use
|
||||
members[author].remaining = remaining - 1;
|
||||
} else { // Disable link
|
||||
delete members[author];
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
|
@ -385,5 +385,9 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
SF.isSharedFolderChannel = function (chanId) {
|
||||
return Object.keys(allSharedFolders).includes(chanId);
|
||||
};
|
||||
|
||||
return SF;
|
||||
});
|
||||
|
|
|
@ -79,7 +79,7 @@ define([
|
|||
GET_HISTORY: Store.getHistory,
|
||||
GET_HISTORY_RANGE: Store.getHistoryRange,
|
||||
IS_NEW_CHANNEL: Store.isNewChannel,
|
||||
REQUEST_PAD_ACCESS: Store.requestPadAccess,
|
||||
CONTACT_PAD_OWNER: Store.contactPadOwner,
|
||||
GIVE_PAD_ACCESS: Store.givePadAccess,
|
||||
BURN_PAD: Store.burnPad,
|
||||
GET_PAD_METADATA: Store.getPadMetadata,
|
||||
|
@ -88,6 +88,7 @@ define([
|
|||
GET_LAST_HASH: Store.getLastHash,
|
||||
GET_SNAPSHOT: Store.getSnapshot,
|
||||
CORRUPTED_CACHE: Store.corruptedCache,
|
||||
DELETE_MAILBOX_MESSAGE: Store.deleteMailboxMessage,
|
||||
// Drive
|
||||
DRIVE_USEROBJECT: Store.userObjectCommand,
|
||||
// Settings,
|
||||
|
|
|
@ -1038,6 +1038,17 @@ define([
|
|||
});
|
||||
}
|
||||
|
||||
// Decrypt hash for invite links
|
||||
Object.keys(members).forEach(function (curve) {
|
||||
var member = members[curve];
|
||||
if (!member.inviteChannel) { return; }
|
||||
if (!member.hash) { return; }
|
||||
if (!teamData.hash) { delete member.hash; return; }
|
||||
try {
|
||||
member.hash = Invite.decryptHash(member.hash, teamData.hash);
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
|
||||
cb(members);
|
||||
});
|
||||
};
|
||||
|
@ -1580,10 +1591,12 @@ define([
|
|||
var message = data.message;
|
||||
var name = data.name;
|
||||
|
||||
/*
|
||||
var password = data.password;
|
||||
//var password = data.password;
|
||||
var hash = data.hash;
|
||||
*/
|
||||
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
|
||||
try {
|
||||
var encryptedHash = Invite.encryptHash(hash, teamData.hash);
|
||||
} catch (e) { console.error(e); }
|
||||
|
||||
// derive { channel, cryptKey} for the preview content channel
|
||||
var previewKeys = Invite.derivePreviewKeys(seeds.preview);
|
||||
|
@ -1595,6 +1608,10 @@ define([
|
|||
// and a placeholder in the roster
|
||||
var ephemeralKeys = Invite.generateKeys();
|
||||
|
||||
// Initial role of the invited users
|
||||
var role = data.role || "VIEWER";
|
||||
var uses = data.uses || 1;
|
||||
|
||||
nThen(function (w) {
|
||||
|
||||
|
||||
|
@ -1652,9 +1669,12 @@ define([
|
|||
};
|
||||
putOpts.metadata.validateKey = sign.validateKey;
|
||||
|
||||
|
||||
|
||||
|
||||
// available only with the link and the content
|
||||
var inviteContent = {
|
||||
teamData: getInviteData(ctx, teamId, false),
|
||||
teamData: getInviteData(ctx, teamId, role === "MEMBER"),
|
||||
ephemeral: {
|
||||
edPublic: ephemeralKeys.edPublic,
|
||||
edPrivate: ephemeralKeys.edPrivate,
|
||||
|
@ -1692,6 +1712,10 @@ define([
|
|||
curvePublic: ephemeralKeys.curvePublic,
|
||||
displayName: data.name,
|
||||
pending: true,
|
||||
remaining: uses,
|
||||
totalUses: uses,
|
||||
role: role,
|
||||
hash: encryptedHash,
|
||||
inviteChannel: inviteKeys.channel,
|
||||
previewChannel: previewKeys.channel,
|
||||
}
|
||||
|
@ -1821,6 +1845,7 @@ define([
|
|||
}));
|
||||
}).nThen(function () {
|
||||
var tempRpc = {};
|
||||
if (!rosterState.remaining || rosterState.remaining === 1) {
|
||||
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
|
||||
if (err) { return; }
|
||||
var rpc = tempRpc.rpc;
|
||||
|
@ -1835,6 +1860,7 @@ define([
|
|||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Add the team to our list and join...
|
||||
joinTeam(ctx, {
|
||||
team: inviteContent.teamData
|
||||
|
|
|
@ -304,6 +304,10 @@ define([
|
|||
var newParent = exp.find(path);
|
||||
var tempName = exp.isFile(element) ? Hash.createChannelId() : key;
|
||||
var newName = exp.getAvailableName(newParent, tempName);
|
||||
if (Array.isArray(newParent)) {
|
||||
newParent.push(element);
|
||||
return;
|
||||
}
|
||||
newParent[newName] = element;
|
||||
};
|
||||
|
||||
|
|
|
@ -171,10 +171,12 @@ define([
|
|||
var data;
|
||||
// apparently some browser extensions send messages to random targets
|
||||
// which can trigger parse errors that interrupt normal behaviour
|
||||
// we therefore log a warning and ignore any messages we can't parse
|
||||
try {
|
||||
data = typeof(msg.data) === "object" ? msg.data : JSON.parse(msg.data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.warn(err);
|
||||
return;
|
||||
}
|
||||
if (typeof(data.ack) !== "undefined") {
|
||||
if (acks[data.txid]) { acks[data.txid](!data.ack); }
|
||||
|
|
|
@ -249,7 +249,6 @@ var factory = function (Util, Rpc) {
|
|||
}, cb);
|
||||
};
|
||||
|
||||
|
||||
cb(e, exp);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -20,7 +20,9 @@ define([
|
|||
'netflux-client': '/bower_components/netflux-websocket/netflux-client',
|
||||
'chainpad-netflux': '/bower_components/chainpad-netflux/chainpad-netflux',
|
||||
'chainpad-listmap': '/bower_components/chainpad-listmap/chainpad-listmap',
|
||||
'cm-extra': '/lib/codemirror-extra-modes'
|
||||
'cm-extra': '/lib/codemirror-extra-modes',
|
||||
// asciidoctor same
|
||||
'asciidoctor': '/lib/asciidoctor/asciidoctor.min'
|
||||
},
|
||||
map: {
|
||||
'*': {
|
||||
|
|
|
@ -80,7 +80,7 @@ define([
|
|||
try {
|
||||
var val = JSON.parse(states[idx].getContent().doc);
|
||||
var md = config.extractMetadata(val);
|
||||
var users = Object.keys(md.users).sort();
|
||||
var users = Object.keys(md.users || {}).sort();
|
||||
return users.join();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
@ -13,6 +13,7 @@ define([
|
|||
Mailbox.create = function (Common) {
|
||||
var mailbox = Common.mailbox;
|
||||
var sframeChan = Common.getSframeChannel();
|
||||
var priv = Common.getMetadataMgr().getPrivateData();
|
||||
|
||||
var execCommand = function (cmd, data, cb) {
|
||||
sframeChan.query('Q_MAILBOX_COMMAND', {
|
||||
|
@ -67,6 +68,14 @@ define([
|
|||
}
|
||||
} else if (data.type === 'reminders') {
|
||||
avatar = h('i.fa.fa-calendar.cp-broadcast.preview');
|
||||
if (priv.app !== 'calendar') { avatar.classList.add('cp-reminder'); }
|
||||
$(avatar).click(function (e) {
|
||||
e.stopPropagation();
|
||||
if (data.content && data.content.handler) {
|
||||
return void data.content.handler();
|
||||
}
|
||||
Common.openURL(Hash.hashToHref('', 'calendar'));
|
||||
});
|
||||
} else if (userData && typeof(userData) === "object" && userData.profile) {
|
||||
avatar = h('span.cp-avatar');
|
||||
Common.displayAvatar($(avatar), userData.avatar, userData.displayName || userData.name);
|
||||
|
@ -120,7 +129,8 @@ define([
|
|||
|
||||
onViewedHandlers.push(function (data) {
|
||||
var hash = data.hash.replace(/"/g, '\\\"');
|
||||
var $notif = $('.cp-notification[data-hash="'+hash+'"]:not(.cp-app-notification-archived)');
|
||||
if (/^REMINDER\|/.test(hash)) { hash = hash.split('-')[0]; }
|
||||
var $notif = $('.cp-notification[data-hash^="'+hash+'"]:not(.cp-app-notification-archived)');
|
||||
if ($notif.length) {
|
||||
$notif.remove();
|
||||
}
|
||||
|
|
|
@ -710,6 +710,12 @@ define([
|
|||
additionalPriv.registeredOnly = true;
|
||||
}
|
||||
|
||||
if (metaObj.priv && Array.isArray(metaObj.priv.mutedChannels)
|
||||
&& metaObj.priv.mutedChannels.includes(secret.channel)) {
|
||||
delete metaObj.priv.mutedChannes;
|
||||
additionalPriv.isChannelMuted = true;
|
||||
}
|
||||
|
||||
var priv = metaObj.priv;
|
||||
var _plan = typeof(priv.plan) === "undefined" ? Utils.LocalStore.getPremium() : priv.plan;
|
||||
var p = Utils.Util.checkRestrictedApp(parsed.type, AppConfig,
|
||||
|
@ -942,7 +948,7 @@ define([
|
|||
var metadata = data.metadata;
|
||||
var add = data.add;
|
||||
var _secret = secret;
|
||||
if (metadata && (metadata.href || metadata.roHref) && !metadata.fakeHref) {
|
||||
if (metadata && (metadata.href || metadata.roHref)) {
|
||||
var _parsed = Utils.Hash.parsePadUrl(metadata.href || metadata.roHref);
|
||||
_secret = Utils.Hash.getSecrets(_parsed.type, _parsed.hash, metadata.password);
|
||||
}
|
||||
|
@ -1010,9 +1016,9 @@ define([
|
|||
});
|
||||
});
|
||||
|
||||
// REQUEST_ACCESS is used both to check IF we can contact an owner (send === false)
|
||||
// CONTACT_OWNER is used both to check IF we can contact an owner (send === false)
|
||||
// AND also to send the request if we want (send === true)
|
||||
sframeChan.on('Q_REQUEST_ACCESS', function (data, cb) {
|
||||
sframeChan.on('Q_CONTACT_OWNER', function (data, cb) {
|
||||
if (readOnly && hashes.editHash) {
|
||||
return void cb({error: 'ALREADYKNOWN'});
|
||||
}
|
||||
|
@ -1030,8 +1036,6 @@ define([
|
|||
var crypto = Crypto.createEncryptor(_secret.keys);
|
||||
nThen(function (waitFor) {
|
||||
// Try to get the owner's mailbox from the pad metadata first.
|
||||
// If it's is an older owned pad, check if the owner is a friend
|
||||
// or an acquaintance (from async-store directly in requestAccess)
|
||||
var todo = function (obj) {
|
||||
owners = obj.owners;
|
||||
|
||||
|
@ -1065,11 +1069,13 @@ define([
|
|||
}));
|
||||
}).nThen(function () {
|
||||
// If we are just checking (send === false) and there is a mailbox field, cb state true
|
||||
// If there is no mailbox, we'll have to check if an owner is a friend in the worker
|
||||
if (!send) { return void cb({state: Boolean(owner)}); }
|
||||
|
||||
Cryptpad.padRpc.requestAccess({
|
||||
Cryptpad.padRpc.contactOwner({
|
||||
send: send,
|
||||
anon: data.anon,
|
||||
query: data.query,
|
||||
msgData: data.msgData,
|
||||
channel: _secret.channel,
|
||||
owner: owner,
|
||||
owners: owners
|
||||
|
|
|
@ -663,49 +663,6 @@ MessengerUI, Messages, Pages) {
|
|||
return $shareBlock;
|
||||
};
|
||||
|
||||
/*
|
||||
var createRequest = function (toolbar, config) {
|
||||
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', {send: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', {send: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 $titleContainer = $('<span>', {
|
||||
'class': TITLE_CLS
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"main_title": "CryptPad: Zusammenarbeit in Echtzeit ohne Preisgabe von Informationen",
|
||||
"main_title": "CryptPad: Zusammenarbeit in Echtzeit mit Schutz der Privatsphäre",
|
||||
"type": {
|
||||
"pad": "Rich Text",
|
||||
"code": "Code",
|
||||
|
@ -831,7 +831,7 @@
|
|||
"team_inviteLinkNote": "Persönliche Nachricht hinzufügen",
|
||||
"team_inviteLinkNoteMsg": "Diese Nachricht wird angezeigt, bevor der Empfänger entscheidet, ob er diesem Team beitreten möchte.",
|
||||
"team_inviteLinkLoading": "Dein Link wird generiert",
|
||||
"team_inviteLinkWarning": "Die erste Person, die auf diesen Link zugreift, kann diesem Team beitreten und dessen Inhalte einsehen. Teile ihn sorgfältig.",
|
||||
"team_inviteLinkWarning": "Personen, die auf diesen Link zugreifen, können diesem Team beitreten und dessen Inhalte einsehen. Teile ihn sorgfältig.",
|
||||
"team_inviteLinkErrorName": "Bitte gib einen Namen für die eingeladene Person ein. Er kann später geändert werden. ",
|
||||
"team_inviteLinkCreate": "Link erstellen",
|
||||
"team_inviteLinkCopy": "Link kopieren",
|
||||
|
@ -1500,5 +1500,83 @@
|
|||
"og_register": "Registriere einen Account auf {0}",
|
||||
"admin_conflictExplanation": "Es gibt zwei Versionen dieses Dokuments. Wenn du die archivierte Version wiederherstellst, wird die aktuelle Version überschrieben. Wenn du die aktuelle Version archivierst, überschreibst du die archivierte Version. Beide Aktionen können nicht rückgängig gemacht werden.",
|
||||
"admin_note": "Abo-Notiz",
|
||||
"admin_planName": "Abo-Name"
|
||||
"admin_planName": "Abo-Name",
|
||||
"calendar_rec_until_count2": "Ereignissen",
|
||||
"calendar_rec_until_date": "Am",
|
||||
"calendar_rec_until_no": "Nie",
|
||||
"calendar_rec_until": "Wiederholen beenden",
|
||||
"calendar_str_filter_month": "Monate: {0}",
|
||||
"calendar_str_filter_weekno": "Wochen: {0}",
|
||||
"calendar_str_filter_day": "Tage: {0}",
|
||||
"calendar_rec_edit": "Dies ist ein sich wiederholendes Ereignis",
|
||||
"calendar_rec_edit_one": "Nur dieses Ereignis bearbeiten",
|
||||
"calendar_rec_edit_from": "Zukünftige Ereignisse bearbeiten",
|
||||
"calendar_rec_edit_all": "Alle Ereignisse bearbeiten",
|
||||
"calendar_rec_stop": "Nicht mehr wiederholen",
|
||||
"calendar_month_last": "letzter Tag",
|
||||
"calendar_rec_until_count": "Nach",
|
||||
"calendar_rec_freq_yearly": "Jahre",
|
||||
"calendar_rec_freq_monthly": "Monate",
|
||||
"calendar_rec_freq_weekly": "Wochen",
|
||||
"calendar_rec_freq_daily": "Tage",
|
||||
"calendar_str_filter": "Filter:",
|
||||
"calendar_rec_txt": "Wiederholen alle",
|
||||
"calendar_rec_custom": "Benutzerdefiniert",
|
||||
"calendar_rec_weekdays": "Täglich an Wochentagen",
|
||||
"calendar_rec_weekend": "Täglich an Wochenenden",
|
||||
"calendar_rec_weekly": "Wöchentlich am {0}",
|
||||
"calendar_rec_daily": "Täglich",
|
||||
"calendar_rec_no": "Einmalig",
|
||||
"calendar_rec": "Wiederholen",
|
||||
"fm_rmFilter": "Filter entfernen",
|
||||
"fm_filterBy": "Filter",
|
||||
"calendar_rec_yearly": "Jährlich am {2}",
|
||||
"calendar_str_yearly": "{0} Jahr(e)",
|
||||
"calendar_str_monthly": "{0} Monat(e)",
|
||||
"calendar_nth_5": "fünften",
|
||||
"calendar_rec_every_date": "Jeden {0}",
|
||||
"calendar_nth_4": "vierten",
|
||||
"calendar_nth_3": "dritten",
|
||||
"calendar_list": "{0}, {1}",
|
||||
"calendar_nth_2": "zweiten",
|
||||
"calendar_list_end": "{0} oder {1}",
|
||||
"calendar_nth_1": "ersten",
|
||||
"calendar_rec_monthly": "Monatlich, Tag {1}",
|
||||
"calendar_str_for": "für {0} Ereignisse",
|
||||
"calendar_str_until": "bis zum {0}",
|
||||
"calendar_str_monthday": "am {0}",
|
||||
"calendar_rec_monthly_nth": "Jeden {0} {1} im Monat",
|
||||
"calendar_rec_yearly_nth": "Jeden {0} {1} im {2}",
|
||||
"calendar_rec_monthly_pick": "An Tagen",
|
||||
"calendar_str_daily": "{0} Tag(e)",
|
||||
"calendar_str_weekly": "{0} Woche(n)",
|
||||
"calendar_str_nthdayofmonth": "am {0} im {1}",
|
||||
"calendar_str_day": "am {0}",
|
||||
"calendar_nth_last": "letzten",
|
||||
"calendar_str_filter_monthday": "Tage im Monat: {0}",
|
||||
"calendar_str_filter_yearday": "Tage im Jahr: {0}",
|
||||
"calendar_rec_updated": "Regel aktualisiert am {0}",
|
||||
"calendar_rec_warn_delall": "Dieses Ereignis wird nicht mehr wiederholt. Das erste Ereignis am {0} bleibt unverändert, alle anderen werden entfernt.",
|
||||
"calendar_rec_warn_del": "Dieses Ereignis wird nicht mehr wiederholt. Zukünftige Ereignisse werden entfernt.",
|
||||
"calendar_removeNotification": "Erinnerung entfernen",
|
||||
"calendar_rec_warn_updateall": "Die Regel für die Wiederholung dieses Ereignisses wurde geändert. Das erste Ereignis am {0} bleibt unverändert, alle anderen werden ersetzt.",
|
||||
"calendar_rec_warn_update": "Die Regel für die Wiederholung dieses Ereignisses wurde geändert. Zukünftige Ereignisse werden ersetzt.",
|
||||
"form_anonymized": "Antworten werden anonymisiert",
|
||||
"form_editable_str": "Absenden",
|
||||
"form_answer_new": "Erneut absenden",
|
||||
"form_exportJSON": "Als JSON exportieren",
|
||||
"form_deleteAll": "Alle löschen",
|
||||
"form_responseNotification": "Neue Antworten für das Formular: <b>{0}</b>",
|
||||
"form_alreadyAnsweredMult": "Du hast dieses Formular beantwortet am:",
|
||||
"form_editable_on_del": "Einmalig und Bearbeiten/Löschen",
|
||||
"form_multiple_edit": "Mehrmals und Bearbeiten/Löschen",
|
||||
"form_multiple": "Mehrmals",
|
||||
"form_editable_off": "Einmalig",
|
||||
"form_allowNotifications": "Benachrichtigungen über neue Antworten",
|
||||
"team_inviteRole": "Erste Rolle",
|
||||
"team_linkUsesInfinite": "(unbegrenzte Verwendungen)",
|
||||
"team_inviteUses": "Erlaubte Verwendung(en) dieses Links (0 = keine Begrenzung)",
|
||||
"team_linkUses": "({0}/{1} verbleibend)",
|
||||
"form_settingsButton": "Formulareinstellungen",
|
||||
"form_editable_on": "Einmalig und Bearbeiten"
|
||||
}
|
||||
|
|
|
@ -138,7 +138,7 @@
|
|||
"fm_newFile": "Nuevo documento",
|
||||
"fm_type": "Tipo",
|
||||
"fm_categoryError": "No se pudo abrir la categoría seleccionada, mostrando la raíz.",
|
||||
"settings_userFeedbackHint1": "CryptPad suministra informaciones muy básicas al servidor, para ayudarnos a mejorar vuestra experiencia.",
|
||||
"settings_userFeedbackHint1": "CryptPad suministra informaciones muy básicas al servidor, para ayudarnos a mejorar tu experiencia. ",
|
||||
"settings_userFeedbackHint2": "El contenido de tus documentos nunca será compartido con el servidor.",
|
||||
"settings_userFeedback": "Activar feedback",
|
||||
"settings_anonymous": "No has iniciado sesión. Tus ajustes se aplicarán sólo a este navegador.",
|
||||
|
@ -322,7 +322,7 @@
|
|||
"tags_add": "Actualizar las etiquetas para los documentos seleccionados",
|
||||
"tags_notShared": "Tus etiquetas no están compartidas con otros usuarios",
|
||||
"tags_duplicate": "Duplicar etiquetas: {0}",
|
||||
"tags_noentry": "No puedes etiquetar un documento eliminado!",
|
||||
"tags_noentry": "¡No puedes etiquetar un documento eliminado!",
|
||||
"slide_invalidLess": "Estilo personalizado no válido",
|
||||
"ok": "OK",
|
||||
"show_help_button": "Mostrar ayuda",
|
||||
|
@ -1400,7 +1400,7 @@
|
|||
"support_warning_document": "Por favor especifica que tipo de documento está causando el problema y provee un <a>identificador de documento</a> o un link",
|
||||
"support_warning_drives": "Ten en cuenta que los/as administradores/as no pueden identificar carpetas y documentos por nombre. Para carpetas compartidas, por favor provee un <a>identificador de documento</a>",
|
||||
"support_warning_account": "Por favor ten en cuenta que los/as administradores/as no pueden restaurar contraseñas. Si has perdido las credenciales para tu cuenta pero sigues con la sesión iniciada, puedes <a>migrar tus datos a una nueva cuenta</a>",
|
||||
"support_warning_prompt": "Por favor escoge la categoría más relevante para tu problema. Esto ayuda a los/as administradores/as a triajar y proveer más sugerencias acerca de que información proveer.",
|
||||
"support_warning_prompt": "Por favor, elige la categoría más relevante para tu problema. Esto ayuda a los administradores a clasificar y proporcionar más sugerencias sobre qué información proporcionar",
|
||||
"info_sourceFlavour": "<a>Código fuente</a> para CryptPad",
|
||||
"info_termsFlavour": "<a>Términos de servicio</a> para esta instancia",
|
||||
"footer_source": "Código fuente",
|
||||
|
@ -1500,5 +1500,65 @@
|
|||
"admin_listMyInstanceHint": "Si tu instancia es adecuada para el uso público puedes consentir a que sea enlistada en los directorios de la red. La telemetría del servidor debe estar activada para que esto tenga algún efecto.",
|
||||
"admin_listMyInstanceTitle": "Listar mi instancia en los directorios públicos",
|
||||
"admin_consentToContactLabel": "Consiento",
|
||||
"admin_consentToContactHint": "La telemetría del servidor incluye el correo de contacto del administrador/a para que así los/as desarrolladores/as puedan notificarte de problemas serios con el software o tu configuración. Nunca será compartido, vendido, o usado por razones de marketing. Consiente al contacto si te gustaría estar informado/a de problemas críticos en tu servidor."
|
||||
"admin_consentToContactHint": "La telemetría del servidor incluye el correo de contacto del administrador/a para que así los/as desarrolladores/as puedan notificarte de problemas serios con el software o tu configuración. Nunca será compartido, vendido, o usado por razones de marketing. Consiente al contacto si te gustaría estar informado/a de problemas críticos en tu servidor.",
|
||||
"calendar_nth_5": "quinto",
|
||||
"calendar_nth_last": "último",
|
||||
"calendar_rec_monthly_nth": "Cada {0} {1} del mes",
|
||||
"calendar_rec_yearly_nth": "Cada {0} {1} de {2}",
|
||||
"calendar_rec_every_date": "Cada {0}",
|
||||
"calendar_nth_4": "cuarto",
|
||||
"calendar_month_last": "último día",
|
||||
"calendar_nth_3": "tercero",
|
||||
"calendar_list": "{0}, {1}",
|
||||
"calendar_nth_2": "segundo",
|
||||
"calendar_list_end": "{0} ó {1}",
|
||||
"calendar_nth_1": "primero",
|
||||
"calendar_str_yearly": "{0} año(s)",
|
||||
"calendar_str_monthly": "{0} mes(es)",
|
||||
"calendar_rec_monthly_pick": "En días",
|
||||
"calendar_str_weekly": "{0} semana(s)",
|
||||
"calendar_rec_until_count2": "tiempos",
|
||||
"calendar_rec_until_count": "Después",
|
||||
"calendar_str_daily": "{0} día(s)",
|
||||
"calendar_rec_until_date": "En",
|
||||
"calendar_str_day": "en {0}",
|
||||
"calendar_rec_until_no": "Nunca",
|
||||
"calendar_rec_until": "Dejar de repetir",
|
||||
"calendar_str_monthday": "en el {0}",
|
||||
"calendar_rec_freq_yearly": "años",
|
||||
"calendar_str_nthdayofmonth": "en el {0} de {1}",
|
||||
"calendar_str_for": "por {0} veces",
|
||||
"calendar_rec_freq_monthly": "meses",
|
||||
"calendar_str_until": "hasta {0}",
|
||||
"calendar_rec_freq_weekly": "semanas",
|
||||
"calendar_rec_freq_daily": "días",
|
||||
"calendar_str_filter": "Filtros:",
|
||||
"calendar_rec_txt": "Repetir cada",
|
||||
"calendar_str_filter_month": "Meses: {0}",
|
||||
"calendar_str_filter_weekno": "Semanas: {0}",
|
||||
"calendar_str_filter_yearday": "Días del año: {0}",
|
||||
"calendar_rec_custom": "Personalizado",
|
||||
"calendar_str_filter_monthday": "Días del mes: {0}",
|
||||
"calendar_rec_weekdays": "Diariamente los días de semana",
|
||||
"calendar_str_filter_day": "Días: {0}",
|
||||
"calendar_rec_edit": "Este es un evento repetido",
|
||||
"calendar_rec_weekend": "Diariamente los fines de semana",
|
||||
"calendar_rec_edit_one": "Solo editar este evento",
|
||||
"calendar_rec_yearly": "Anualmente en {2}",
|
||||
"calendar_rec_edit_from": "Editar futuros eventos",
|
||||
"calendar_rec_monthly": "Mensual, día {1}",
|
||||
"calendar_rec_edit_all": "Editar todos los eventos",
|
||||
"calendar_rec_weekly": "Semanal en {0}",
|
||||
"calendar_rec_stop": "Dejar de repetir",
|
||||
"calendar_rec_daily": "Diario",
|
||||
"calendar_rec_updated": "Regla actualizada en {0}",
|
||||
"calendar_rec_no": "Una vez",
|
||||
"calendar_rec": "Repetir",
|
||||
"fm_rmFilter": "Eliminar filtro",
|
||||
"fm_filterBy": "Filtro",
|
||||
"calendar_removeNotification": "Eliminar recordatorio",
|
||||
"calendar_rec_warn_updateall": "La regla para repetir este evento ha sido modificada. El primer evento en {0} será mantenido, todos los demás serán sustituidos.",
|
||||
"calendar_rec_warn_update": "La regla para repetir este evento ha sido modificada. Los eventos futuros serán sustituidos.",
|
||||
"calendar_rec_warn_delall": "Este evento ya no se va a repetir. El primer evento en {0} se mantendrá, todos los demás serán eliminados.",
|
||||
"calendar_rec_warn_del": "Este evento ya no se va a repetir. Los eventos futuros serán eliminados."
|
||||
}
|
||||
|
|
|
@ -1397,7 +1397,7 @@
|
|||
"admin_enableDiskMeasurementsHint": "Gaituta badago, JSON API amaierako puntu bat agertuko da <code>/api/profiling</code> azpian. Honek diskoko I/O-ren neurketa martxan mantentzen du beheko denbora-leihoan. Ezarpen honek zerbitzariaren errendimenduan eragina izan dezake eta datu sentikorrak ager ditzake. Ezarpen hau desgaituta uztea gomendatzen da zertan ari zaren jakin ezean.",
|
||||
"admin_enableDiskMeasurementsTitle": "Neurtu diskoaren errendimendua",
|
||||
"admin_infoNotice2": "Ikus 'Sarea' fitxa xehetasun gehiagorako.",
|
||||
"admin_infoNotice1": "Erabili hurrengo eremuak zure instantzia deskribatzeko. Informazio hau CryptPad-en etorkizuneko bertsio batean instantziako lehen orrialdean erabiliko da. Zerbitzariaren telemetriaren zati gisa bidaltzen da CryptPad instantzia publikoen zerrendan sartzea aukeratzen baduzu soilik.",
|
||||
"admin_infoNotice1": "Erabili hurrengo eremuak zure instantzia deskribatzeko. Informazio hau instantziako portadan erabiltzen da. Zerbitzariaren telemetriaren zati gisa ere bidaltzen da CryptPad instantzia publikoen zerrendan sartzea aukeratzen baduzu.",
|
||||
"admin_reviewCheckupNotice": "<a>Egiaztapen</a>-orria berrikustea gomendatzen da instantzia hau behar bezala konfiguratuta dagoela baieztatzeko.",
|
||||
"admin_cacheEvictionRequired": "Zerbitzaria ezarpen berriarekin eguneratu da. Mesedez, erabili <b>Flush cachea</b> botoia aldaketa hau erabiltzaile guztientzat ikusgai egongo dela ziurtatzeko.",
|
||||
"fivehundred_internalServerError": "Barneko zerbitzari-errorea",
|
||||
|
@ -1452,7 +1452,7 @@
|
|||
"admin_pinLogArchived": "Saioa hasteko pin-a artxibatuta dago",
|
||||
"admin_pinLogAvailable": "Saioa hasteko pin-a eskuragarri dago",
|
||||
"admin_fileCount": "Fitxategien kopurua",
|
||||
"admin_channelCount": "Kanalen kopurua",
|
||||
"admin_channelCount": "Dokumentuen kopurua",
|
||||
"admin_storageUsage": "Biltegiratutako datuak",
|
||||
"admin_note": "Planaren oharra",
|
||||
"admin_planName": "Planaren izena",
|
||||
|
@ -1462,14 +1462,14 @@
|
|||
"admin_blockMetadataPlaceholder": "Blokearen URL absolutua edo erlatiboa",
|
||||
"admin_blockMetadataHint": "Saioa hasteko blokea CryptPad-en saioa hasteko aukera ematen duen erabiltzaile-izena + pasahitzaren konbinazioa",
|
||||
"admin_blockMetadataTitle": "Saioa hasteko blokearen informazioa",
|
||||
"admin_channelArchived": "Kanala artxibatuta dago",
|
||||
"admin_channelAvailable": "Kanala eskuragarri dago",
|
||||
"admin_channelArchived": "Artxibatuta",
|
||||
"admin_channelAvailable": "Eskuragarria",
|
||||
"admin_currentlyOpen": "Une honetan zabalik",
|
||||
"admin_documentCreationTime": "Sortze-data",
|
||||
"admin_documentModifiedTime": "Azken aldaketa",
|
||||
"admin_documentMetadata": "Uneko metadatuak",
|
||||
"admin_documentSize": "Dokumentuaren tamaina",
|
||||
"admin_documentMetadataHint": "Kontsultatu kanal edo fitxategi bat bere id edo URL bidez",
|
||||
"admin_documentMetadataHint": "Kontsultatu dokumentu edo fitxategi bat bere ID edo URL bidez",
|
||||
"admin_documentMetadataTitle": "Dokumentuaren informazioa",
|
||||
"admin_accountMetadataHint": "Sartu erabiltzailearen gako publikoa bere kontuari buruzko datuak eskuratzeko.",
|
||||
"admin_accountMetadataTitle": "Kontuaren informazioa",
|
||||
|
@ -1482,5 +1482,83 @@
|
|||
"ui_none": "bat ere ez",
|
||||
"admin_uptimeTitle": "Abiarazte ordua",
|
||||
"admin_cat_database": "Datu-basea",
|
||||
"admin_uptimeHint": "Zerbitzaria abiarazi den data eta ordua"
|
||||
"admin_uptimeHint": "Zerbitzaria abiarazi den data eta ordua",
|
||||
"admin_documentConflict": "Artxibatu/berreskuratu",
|
||||
"og_encryptedAppType": "Enkriptatuta {0}",
|
||||
"ui_jsRequired": "JavaScript gaituta egon behar da zure nabigatzailean enkriptaketak egiteko",
|
||||
"og_features": "{0} funtzio",
|
||||
"og_pricing": "{0} prezio",
|
||||
"og_contact": "{0} kontaktu",
|
||||
"og_register": "Erregistratu kontu bat {0}-n",
|
||||
"og_login": "Hasi saioa {0}-n",
|
||||
"og_default": "CryptPad: muturretik muturrera enkriptatutako lankidetza suitea",
|
||||
"admin_reportContent": "Eman eduki baten berri",
|
||||
"admin_lastPinTime": "Azkeneko pinaren jardueraren ordua",
|
||||
"admin_firstPinTime": "Lehenengo pinaren jardueraren ordua",
|
||||
"ui_false": "faltsua",
|
||||
"ui_true": "egia",
|
||||
"form_anonymous": "Gonbidatuen sarbidea (saioa hasi gabe)",
|
||||
"calendar_rec_weekly": "Astero {0}-n",
|
||||
"calendar_rec_stop": "Utzi errepikatzeari",
|
||||
"calendar_rec_daily": "Egunero",
|
||||
"calendar_rec_updated": "Araua {0}-n eguneratu da",
|
||||
"calendar_rec_no": "Behin",
|
||||
"calendar_rec": "Errepikatu",
|
||||
"fm_rmFilter": "Kendu iragazkia",
|
||||
"fm_filterBy": "Iragazkia",
|
||||
"admin_conflictExplanation": "Dokumentu honen bi bertsio daude. Artxibatutako bertsioa berreskuratzeak uneko bertsioa gainidatziko du. Uneko bertsioa artxibatzeak artxibatutako bertsioa gainidatziko du. Ekintza bat ere ezin da desegin.",
|
||||
"calendar_rec_until_no": "Inoiz ez",
|
||||
"calendar_rec_until": "Utzi errepikatzeari",
|
||||
"calendar_str_monthday": "{0}n",
|
||||
"calendar_rec_freq_yearly": "urte",
|
||||
"calendar_str_nthdayofmonth": "{1}ko {0}-n",
|
||||
"calendar_str_for": "{0} aldiz",
|
||||
"calendar_rec_freq_monthly": "hilabete",
|
||||
"calendar_str_until": "{0} arte",
|
||||
"calendar_rec_freq_weekly": "aste",
|
||||
"calendar_rec_freq_daily": "egun",
|
||||
"calendar_str_filter": "Iragazkiak:",
|
||||
"calendar_rec_txt": "Errepikatu",
|
||||
"calendar_str_filter_month": "Hilabeteak: {0}",
|
||||
"calendar_str_filter_weekno": "Asteak: {0}",
|
||||
"calendar_str_filter_yearday": "Urteko egunak: {0}",
|
||||
"calendar_rec_custom": "Pertsonalizatua",
|
||||
"calendar_str_filter_monthday": "Hileko egunak: {0}",
|
||||
"calendar_rec_weekdays": "Egunero astegunetan",
|
||||
"calendar_str_filter_day": "Egunak: {0}",
|
||||
"calendar_rec_edit": "Asteburuetan egunero",
|
||||
"calendar_rec_weekend": "Asteburuetan egunero",
|
||||
"calendar_rec_edit_one": "Editatu gertaera hau soilik",
|
||||
"calendar_rec_yearly": "Urtero {2}-n",
|
||||
"calendar_rec_edit_from": "Editatu etorkizuneko gertaerak",
|
||||
"calendar_rec_monthly": "Hilero, {1} eguna",
|
||||
"calendar_rec_edit_all": "Editatu gertaera guztiak",
|
||||
"og_teamDrive": "Taldeko diskoa",
|
||||
"calendar_removeNotification": "Kendu abisua",
|
||||
"calendar_rec_warn_updateall": "Gertaera hau errepikatzeko araua aldatu zen. {0}-ko lehen gertaera mantenduko da, beste guztiak ordezkatuko dira.",
|
||||
"calendar_rec_warn_update": "Gertaera hau errepikatzeko araua aldatu zen. Etorkizuneko gertaerak ordezkatuko dira.",
|
||||
"calendar_rec_warn_delall": "Gertaera hau ez da gehiago errepikatuko. {0}-ko lehen gertaera mantenduko da, beste guztiak kenduko dira.",
|
||||
"calendar_rec_warn_del": "Gertaera hau ez da gehiago errepikatuko. Etorkizuneko gertaerak kendu egingo dira.",
|
||||
"calendar_nth_5": "bosgarren",
|
||||
"calendar_nth_3": "hirugarren",
|
||||
"calendar_nth_1": "lehenengo",
|
||||
"calendar_nth_2": "bigarren",
|
||||
"calendar_nth_4": "laugarren",
|
||||
"calendar_nth_last": "azken",
|
||||
"calendar_rec_monthly_nth": "Hilabeteko {0} {1} behin",
|
||||
"calendar_rec_yearly_nth": "{2}ko {0} {1} behin",
|
||||
"calendar_rec_every_date": "{0} behin",
|
||||
"calendar_month_last": "azken eguna",
|
||||
"calendar_list": "{0}, {1}",
|
||||
"calendar_list_end": "{0} edo {1}",
|
||||
"calendar_str_yearly": "{0} urte",
|
||||
"calendar_str_monthly": "{0} hilabete",
|
||||
"calendar_rec_monthly_pick": "Egunetan",
|
||||
"calendar_str_weekly": "{0} aste",
|
||||
"calendar_rec_until_count2": "aldiz",
|
||||
"calendar_rec_until_count": "Ondoren",
|
||||
"calendar_str_daily": "{0} egun",
|
||||
"calendar_rec_until_date": "Hemen",
|
||||
"calendar_str_day": "hemen {0}",
|
||||
"admin_generatedAt": "Denbora-markaren berri eman"
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{}
|
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,13 @@
|
|||
"pad": "रिच टेक्स्ट",
|
||||
"code": "कोड",
|
||||
"poll": "मतदान",
|
||||
"kanban": "कानबन"
|
||||
"kanban": "कानबन",
|
||||
"todo": "टुडू",
|
||||
"media": "मीडिया",
|
||||
"file": "फ़ाइल",
|
||||
"whiteboard": "व्हाइटबोर्ड",
|
||||
"drive": "क्रिप्टड्राइव",
|
||||
"slide": "मार्कडाउन स्लाइड्स"
|
||||
},
|
||||
"main_title": "क्रिप्टपैड: शून्य ज्ञान, सहयोगात्मक रीयल टाइम संपादन"
|
||||
}
|
||||
|
|
|
@ -1495,5 +1495,65 @@
|
|||
"admin_blockKey": "ブロックの公開鍵",
|
||||
"admin_blockMetadataPlaceholder": "ブロックの絶対または相対URL",
|
||||
"admin_restoreReason": "復元の理由を指定し、確認して続行してください",
|
||||
"admin_archiveReason": "アーカイブの理由を指定し、確認して続行してください"
|
||||
"admin_archiveReason": "アーカイブの理由を指定し、確認して続行してください",
|
||||
"calendar_nth_1": "第1",
|
||||
"calendar_list_end": "{0}または{1}",
|
||||
"calendar_nth_2": "第2",
|
||||
"calendar_list": "{0}、{1}",
|
||||
"calendar_nth_3": "第3",
|
||||
"calendar_nth_last": "最終",
|
||||
"calendar_nth_5": "第5",
|
||||
"calendar_rec_every_date": "毎{0}",
|
||||
"calendar_nth_4": "第4",
|
||||
"calendar_month_last": "最終日",
|
||||
"calendar_str_yearly": "{0}年",
|
||||
"calendar_str_weekly": "{0}週",
|
||||
"calendar_str_monthly": "{0}月",
|
||||
"calendar_rec_until_count2": "回",
|
||||
"calendar_str_daily": "{0}日",
|
||||
"calendar_str_filter_month": "月:{0}",
|
||||
"calendar_str_filter_weekno": "週:{0}",
|
||||
"calendar_rec_custom": "ユーザー定義",
|
||||
"calendar_rec_edit": "これは繰り返すイベントです",
|
||||
"calendar_rec_weekend": "毎週末",
|
||||
"calendar_rec_edit_one": "このイベントのみを編集",
|
||||
"calendar_rec_yearly": "毎年{2}",
|
||||
"calendar_rec_edit_from": "今後のイベントを編集",
|
||||
"calendar_rec_monthly": "毎月{1}日",
|
||||
"calendar_rec_edit_all": "全てのイベントを編集",
|
||||
"calendar_rec_weekly": "毎週{0}",
|
||||
"calendar_rec_stop": "繰り返しを停止",
|
||||
"calendar_rec_daily": "毎日",
|
||||
"calendar_rec_until": "繰り返しを停止",
|
||||
"calendar_str_monthday": "{0}に",
|
||||
"calendar_rec_freq_yearly": "年",
|
||||
"calendar_str_for": "{0}回まで",
|
||||
"calendar_rec_freq_monthly": "月",
|
||||
"calendar_str_until": "{0}まで",
|
||||
"calendar_rec_freq_weekly": "週",
|
||||
"calendar_rec_freq_daily": "日",
|
||||
"calendar_str_filter": "フィルター:",
|
||||
"calendar_rec_txt": "繰り返しの頻度",
|
||||
"calendar_rec_no": "一度",
|
||||
"calendar_rec": "繰り返す",
|
||||
"fm_rmFilter": "フィルターを削除",
|
||||
"fm_filterBy": "フィルター",
|
||||
"admin_conflictExplanation": "このドキュメントには2つのバージョンがあります。アーカイブされたバージョンを復元すると、現在のバージョンが上書きされます。現在のバージョンをアーカイブすると、アーカイブ済のバージョンが上書きされます。どちらのアクションも取り消しできません。",
|
||||
"admin_documentConflict": "アーカイブ/復元",
|
||||
"calendar_rec_until_count": "以後",
|
||||
"form_editable_str": "送信",
|
||||
"form_multiple": "複数回",
|
||||
"form_editable_off": "一度のみ",
|
||||
"form_allowNotifications": "新しい回答の通知",
|
||||
"form_answer_new": "再送信",
|
||||
"form_deleteAll": "全て削除",
|
||||
"form_exportJSON": "JSONでエクスポート",
|
||||
"team_inviteRole": "初期の役割",
|
||||
"calendar_rec_warn_delall": "この予定は繰り返しません。{0}の予定を残し、それ以外は削除されます。",
|
||||
"calendar_rec_warn_del": "この予定は今後繰り返しません。将来の予定が削除されます。",
|
||||
"calendar_rec_weekdays": "平日全て",
|
||||
"calendar_str_filter_day": "日:{0}",
|
||||
"calendar_rec_warn_update": "このイベントの繰り返しに関するルールが変更されました。 今後のイベントは置き換えられます。",
|
||||
"calendar_rec_warn_updateall": "このイベントの繰り返しに関するルールが変更されました。 {0}の最初のイベントは維持されます。他の全ては置き換えられます。",
|
||||
"calendar_removeNotification": "リマインダーを削除"
|
||||
}
|
||||
|
|
|
@ -833,7 +833,7 @@
|
|||
"team_inviteLinkNote": "Add a personal message",
|
||||
"team_inviteLinkNoteMsg": "This message will be shown before the recipient decides whether to join this team.",
|
||||
"team_inviteLinkLoading": "Generating your link",
|
||||
"team_inviteLinkWarning": "The first person to access this link will be able to join this team and view its contents. Share it carefully.",
|
||||
"team_inviteLinkWarning": "People who access this link will be able to join this team and view its contents. Share it carefully.",
|
||||
"team_inviteLinkErrorName": "Please add a name for the person you're inviting. They can change it later. ",
|
||||
"team_inviteLinkCreate": "Create link",
|
||||
"team_inviteLinkCopy": "Copy link",
|
||||
|
@ -1500,5 +1500,83 @@
|
|||
"ui_jsRequired": "JavaScript must be enabled to perform encryption in your browser",
|
||||
"og_encryptedAppType": "Encrypted {0}",
|
||||
"admin_documentConflict": "Archive/restore",
|
||||
"admin_conflictExplanation": "Two versions of this document exist. Restoring the archived version will overwrite the live version. Archiving the live version will overwrite the archived version. Neither action can be undone."
|
||||
"admin_conflictExplanation": "Two versions of this document exist. Restoring the archived version will overwrite the live version. Archiving the live version will overwrite the archived version. Neither action can be undone.",
|
||||
"fm_filterBy": "Filter",
|
||||
"fm_rmFilter": "Remove filter",
|
||||
"calendar_rec": "Repeat",
|
||||
"calendar_rec_no": "One time",
|
||||
"calendar_rec_updated": "Rule updated on {0}",
|
||||
"calendar_rec_daily": "Daily",
|
||||
"calendar_rec_stop": "Stop repeating",
|
||||
"calendar_rec_weekly": "Weekly on {0}",
|
||||
"calendar_rec_edit_all": "Edit all events",
|
||||
"calendar_rec_monthly": "Monthly, day {1}",
|
||||
"calendar_rec_edit_from": "Edit future events",
|
||||
"calendar_rec_yearly": "Yearly on {2}",
|
||||
"calendar_rec_edit_one": "Edit only this event",
|
||||
"calendar_rec_weekend": "Daily on weekends",
|
||||
"calendar_rec_edit": "This is a repeating event",
|
||||
"calendar_str_filter_day": "Days: {0}",
|
||||
"calendar_rec_weekdays": "Daily on weekdays",
|
||||
"calendar_str_filter_monthday": "Days of month: {0}",
|
||||
"calendar_rec_custom": "Custom",
|
||||
"calendar_str_filter_yearday": "Days of year: {0}",
|
||||
"calendar_str_filter_weekno": "Weeks: {0}",
|
||||
"calendar_str_filter_month": "Months: {0}",
|
||||
"calendar_rec_txt": "Repeat every",
|
||||
"calendar_str_filter": "Filters:",
|
||||
"calendar_rec_freq_daily": "days",
|
||||
"calendar_rec_freq_weekly": "weeks",
|
||||
"calendar_str_until": "until {0}",
|
||||
"calendar_rec_freq_monthly": "months",
|
||||
"calendar_str_for": "for {0} times",
|
||||
"calendar_str_nthdayofmonth": "on the {0} of {1}",
|
||||
"calendar_rec_freq_yearly": "years",
|
||||
"calendar_str_monthday": "on the {0}",
|
||||
"calendar_rec_until": "Stop Repeating",
|
||||
"calendar_rec_until_no": "Never",
|
||||
"calendar_str_day": "on {0}",
|
||||
"calendar_rec_until_date": "On",
|
||||
"calendar_str_daily": "{0} day(s)",
|
||||
"calendar_rec_until_count": "After",
|
||||
"calendar_rec_until_count2": "times",
|
||||
"calendar_str_weekly": "{0} week(s)",
|
||||
"calendar_rec_monthly_pick": "On days",
|
||||
"calendar_str_monthly": "{0} month(s)",
|
||||
"calendar_str_yearly": "{0} year(s)",
|
||||
"calendar_nth_1": "first",
|
||||
"calendar_list_end": "{0} or {1}",
|
||||
"calendar_nth_2": "second",
|
||||
"calendar_list": "{0}, {1}",
|
||||
"calendar_nth_3": "third",
|
||||
"calendar_month_last": "last day",
|
||||
"calendar_nth_4": "fourth",
|
||||
"calendar_rec_every_date": "Every {0}",
|
||||
"calendar_rec_yearly_nth": "Every {0} {1} of {2}",
|
||||
"calendar_rec_monthly_nth": "Every {0} {1} of the month",
|
||||
"calendar_nth_last": "last",
|
||||
"calendar_nth_5": "fifth",
|
||||
"calendar_rec_warn_del": "This event will no longer repeat. Future events will be removed.",
|
||||
"calendar_rec_warn_delall": "This event will no longer repeat. The first event on {0} will be kept, all others will be removed.",
|
||||
"calendar_rec_warn_update": "The rule for repeating this event was modified. Future events will be replaced.",
|
||||
"calendar_rec_warn_updateall": "The rule for repeating this event was modified. The first event on {0} will be kept, all others will be replaced.",
|
||||
"calendar_removeNotification": "Remove reminder",
|
||||
"team_inviteRole": "Initial role",
|
||||
"team_inviteUses": "Use(s) allowed for this link (0 = no limit)",
|
||||
"form_exportJSON": "Export to JSON",
|
||||
"form_alreadyAnsweredMult": "You responded to this form on:",
|
||||
"form_responseNotification": "New responses to form: <b>{0}</b>",
|
||||
"form_deleteAll": "Delete all",
|
||||
"form_answer_new": "Submit again",
|
||||
"form_allowNotifications": "Notifications for new responses",
|
||||
"form_editable_off": "One time only",
|
||||
"form_editable_on_del": "One time and edit/delete",
|
||||
"form_multiple": "Multiple times",
|
||||
"form_multiple_edit": "Multiple times and edit/delete",
|
||||
"form_editable_str": "Submission",
|
||||
"team_linkUsesInfinite": "(unlimited uses)",
|
||||
"team_linkUses": "({0}/{1} remaining)",
|
||||
"form_anonymized": "Responses are anonymized",
|
||||
"form_settingsButton": "Form settings",
|
||||
"form_editable_on": "One time and edit"
|
||||
}
|
||||
|
|
|
@ -180,7 +180,7 @@
|
|||
"contacts_confirmRemove": "Czy na pewno chcesz usunąć <em>{0}</em> ze swoich kontaktów?",
|
||||
"contacts_remove": "Usuń ten kontakt",
|
||||
"contacts_send": "Wyślij",
|
||||
"contacts_request": "<em>{0}</em> chciałby dodać Cię jako kontakt. <b>Akceptujesz</b>?",
|
||||
"contacts_request": "<em>{0}</em> chciał(a)by dodać Cię jako kontakt. <b>Akceptujesz</b>?",
|
||||
"contacts_rejected": "Zaproszenie do kontaktu odrzucone",
|
||||
"contacts_added": "Zaproszenie do kontaktu przyjęte.",
|
||||
"contacts_title": "Kontakty",
|
||||
|
@ -1397,5 +1397,6 @@
|
|||
"admin_descriptionTitle": "Opis istancji",
|
||||
"ui_saved": "{0} zapisano",
|
||||
"admin_nameHint": "Wyświetlana nazwa dla tego urządzenia z listy publicznych instacji na stronie cryptpad.org",
|
||||
"admin_archiveNote": "Notatka"
|
||||
"admin_archiveNote": "Notatka",
|
||||
"common_connectionLost": "<b>Utracono połączenie z serwerem</b><br>Dopóki połączenie nie wróci, włączony będzie tryb tylko do odczytu."
|
||||
}
|
||||
|
|
|
@ -88,14 +88,14 @@
|
|||
"newButton": "Создать",
|
||||
"uploadButton": "Загрузить файлы",
|
||||
"uploadButtonTitle": "Загрузить новый файл в ваш CryptDrive",
|
||||
"saveTemplateButton": "Сохранить как образец",
|
||||
"saveTemplatePrompt": "Выбрать название для образца",
|
||||
"templateSaved": "Образец сохранен!",
|
||||
"selectTemplate": "Выберите образец или нажмите Esc",
|
||||
"useTemplate": "Начать с образца?",
|
||||
"useTemplateOK": "Выбрать образец (Enter)",
|
||||
"template_import": "Импортировать образец",
|
||||
"template_empty": "Образцы отсутствуют",
|
||||
"saveTemplateButton": "Сохранить как шаблон",
|
||||
"saveTemplatePrompt": "Выберите название для шаблона",
|
||||
"templateSaved": "Шаблон сохранен!",
|
||||
"selectTemplate": "Выберите шаблон или нажмите Esc",
|
||||
"useTemplate": "Использовать шаблон?",
|
||||
"useTemplateOK": "Выбрать шаблон (Enter)",
|
||||
"template_import": "Импортировать шаблон",
|
||||
"template_empty": "Шаблоны отсутствуют",
|
||||
"presentButtonTitle": "Начать режим презентации",
|
||||
"backgroundButtonTitle": "Изменить фоновый цвет в презентации",
|
||||
"colorButtonTitle": "Изменить цвет шрифта в презентации",
|
||||
|
@ -357,7 +357,7 @@
|
|||
"settings_exportCancel": "Вы уверены, что хотите отменить экспорт? В следующий раз вам придется начинать все сначала.",
|
||||
"settings_export_reading": "Читаем ваше хранилище...",
|
||||
"settings_export_download": "Скачиваем и расшифровываем ваши документы...",
|
||||
"contacts_request": "<em>{0}</em> хотел бы добавить вас в список контактов. <b>Принять </b>?",
|
||||
"contacts_request": "<em>{0}</em> хотел(а) бы добавить Вас в свой список контактов. <b>Согласны</b>?",
|
||||
"contacts_confirmRemove": "Вы уверены, что хотите удалить <em>1{0}</em>2 из ваших контактов?",
|
||||
"register_acceptTerms": "Я принимаю <a>условия использования</a>",
|
||||
"register_warning": "Внимание",
|
||||
|
@ -412,7 +412,7 @@
|
|||
"burnAfterReading_generateLink": "Нажмите на кнопку ниже, чтобы создать ссылку.",
|
||||
"upload_size": "Размер",
|
||||
"upload_pending": "Ожидайте",
|
||||
"upload_tooLargeBrief": "Размер файла превышает лимит в {0}МБ",
|
||||
"upload_tooLargeBrief": "Размер файла превышает лимит в {0}МБ установленный на этом Диске",
|
||||
"upload_notEnoughSpaceBrief": "Недостаточно места",
|
||||
"upload_notEnoughSpace": "Недостаточно места для этого файла на вашем CryptDrive.",
|
||||
"settings_cursorShareTitle": "Делиться позицией моего курсора",
|
||||
|
@ -745,7 +745,7 @@
|
|||
"feedback_optout": "Если вы хотите отказаться, посетите <a>страницу настроек пользователя</a>, где вы найдете флажок для включения или отключения обратной связи с пользователем.",
|
||||
"feedback_privacy": "Мы заботимся о Вашей конфиденциальности и в то же время хотим, чтобы CryptPad был очень простым в использовании. Мы используем этот файл, чтобы выяснить, какие функции пользовательского интерфейса важны для наших пользователей, запрашивая его вместе с параметром, указывающим, какое действие было предпринято.",
|
||||
"feedback_about": "Если Вы читаете это, Вам, вероятно, было любопытно, почему CryptPad запрашивает веб-страницы при выполнении определенных действий.",
|
||||
"help_genericMore": "Узнайте больше о том, как CryptPad может работать для вас, прочитав нашу <a>Документацию</a>.",
|
||||
"help_genericMore": "Узнайте больше о том, как CryptPad может работать для вас, прочитав нашу <a>Документацию</a>",
|
||||
"header_logoTitle": "Перейти в Ваш CryptDrive",
|
||||
"features_f_cryptdrive0_note": "Возможность сохранять в вашем браузере посещенные документы, чтобы иметь возможность открывать их позже",
|
||||
"settings_padOpenLinkLabel": "Разрешить открытие прямой ссылки",
|
||||
|
@ -760,7 +760,7 @@
|
|||
"team_inviteLinkCopy": "Копировать ссылку",
|
||||
"team_inviteLinkCreate": "Создать ссылку",
|
||||
"team_inviteLinkErrorName": "Добавьте имя человека, которого Вы приглашаете. Они могут изменить это позже. ",
|
||||
"team_inviteLinkWarning": "Первый, кто получит доступ к этой ссылке, сможет присоединиться к этой команде и просмотреть её содержимое. Делитесь ею с осторожностью.",
|
||||
"team_inviteLinkWarning": "Те, кто получат к эту ссылку, смогут присоединиться к этой команде и просмотреть её содержимое. Делитесь ссылкой с осторожностью.",
|
||||
"team_inviteLinkLoading": "Создание Вашей ссылки",
|
||||
"team_inviteLinkNoteMsg": "Это сообщение будет показано до того, как получатель решит, присоединиться ли к этой команде.",
|
||||
"team_inviteLinkNote": "Добавьте личное сообщение",
|
||||
|
@ -939,7 +939,7 @@
|
|||
"admin_authError": "Только администраторы могут получить доступ к этой странице",
|
||||
"survey": "Опрос CryptPad",
|
||||
"crowdfunding_popup_text": "<h3>Нам нужна ваша помощь!</h3>Чтобы быть уверенными, что CryptPad активно развивается - рассмотрите возможность поддержки проекта через страницу OpenCollective, где Вы можете увидеть нашу <b>Дорожную карту</b> и<b>Цели финансирования</b>.",
|
||||
"crowdfunding_button2": "Помочь CryptPad",
|
||||
"crowdfunding_button2": "Помочь деньгами",
|
||||
"autostore_notAvailable": "Вы должны сохранить этот документ на Вашем CryptDrive, прежде чем сможете использовать эту функцию.",
|
||||
"autostore_forceSave": "Сохраните файл в Вашем CryptDrive",
|
||||
"autostore_saved": "Документ успешно сохранен на вашем CryptDrive!",
|
||||
|
@ -1166,9 +1166,9 @@
|
|||
"support_cat_bug": "Сообщить об ошибке",
|
||||
"support_cat_data": "Пропажа содержимого",
|
||||
"support_cat_account": "Учетная запись пользователя",
|
||||
"info_privacyFlavour": "Наша <a>политика конфиденциальности</a> описывает, как мы обрабатываем Ваши данные.",
|
||||
"info_privacyFlavour": "<a>Политика конфиденциальности</a> этого экземпляра CryptPad",
|
||||
"user_about": "О CryptPad",
|
||||
"info_imprintFlavour": "<a>Правовая информация об администраторах данного экземпляра</a>.",
|
||||
"info_imprintFlavour": "<a>Правовая информация</a> об администраторах данного экземпляра",
|
||||
"settings_safeLinkDefault": "Безопасные ссылки теперь включены по умолчанию. Для копирования ссылок используйте меню <i></i><b>Поделиться</b>, а не адресную строку браузера.",
|
||||
"slide_textCol": "Цвет текста",
|
||||
"slide_backCol": "Цвет фона",
|
||||
|
@ -1386,6 +1386,196 @@
|
|||
"bounce_confirm": "Вы покинете: {0}\n\nВы точно хотите перейти к \"{1}\"?",
|
||||
"ui_restore": "Восстановить",
|
||||
"ui_archive": "Архивировать",
|
||||
"ui_undefined": "неизвестный",
|
||||
"admin_documentType": "Тип"
|
||||
"ui_undefined": "неизвестно",
|
||||
"admin_documentType": "Тип документа",
|
||||
"form_settingsButton": "Настройки формы",
|
||||
"form_anonymized": "Ответы анонимизированы",
|
||||
"team_linkUses": "({0}/{1} осталось)",
|
||||
"team_linkUsesInfinite": "(неограниченное использование)",
|
||||
"form_editable_str": "Подача",
|
||||
"form_multiple_edit": "Многократно и редактировать/удалять",
|
||||
"form_multiple": "Многократно",
|
||||
"form_editable_on_del": "Один раз и отредактировать/удалить",
|
||||
"form_editable_off": "Только один раз",
|
||||
"form_allowNotifications": "Уведомления о новых ответах",
|
||||
"form_answer_new": "Отправить снова",
|
||||
"form_deleteAll": "Удалить все",
|
||||
"form_responseNotification": "Новые ответы на форму: <b>{0}</b>",
|
||||
"form_alreadyAnsweredMult": "Вы ответили на эту форму:",
|
||||
"form_exportJSON": "Экспорт в JSON",
|
||||
"team_inviteUses": "Сколько раз можно использовать эту ссылку (0 = без ограничений)",
|
||||
"team_inviteRole": "Начальная роль",
|
||||
"calendar_removeNotification": "Удалить напоминание",
|
||||
"calendar_rec_warn_updateall": "Правило для повторения этого события было изменено. Первое событие на {0} будет сохранено, все остальные будут заменены.",
|
||||
"calendar_rec_warn_update": "Правило для повторения этого события было изменено. Будущие события будут заменены.",
|
||||
"calendar_rec_warn_delall": "Это событие больше не повторится. Первое событие на {0} будет сохранено, все остальные будут удалены.",
|
||||
"calendar_rec_warn_del": "Это событие больше не повторится. Будущие события будут удалены.",
|
||||
"calendar_nth_5": "пятый",
|
||||
"calendar_nth_last": "последний",
|
||||
"calendar_rec_monthly_nth": "Каждый {0} {1} месяца",
|
||||
"calendar_rec_yearly_nth": "Каждый {0} {1} {2}",
|
||||
"calendar_rec_every_date": "Каждый {0}",
|
||||
"calendar_nth_4": "четвёртый",
|
||||
"calendar_month_last": "последний день",
|
||||
"calendar_nth_3": "третий",
|
||||
"calendar_list": "{0}, {1}",
|
||||
"calendar_nth_2": "второй",
|
||||
"calendar_list_end": "{0} или {1}",
|
||||
"calendar_nth_1": "первый",
|
||||
"calendar_str_yearly": "{0} год(ы)",
|
||||
"calendar_str_monthly": "{0} месяц(ы)",
|
||||
"calendar_rec_monthly_pick": "По дням",
|
||||
"calendar_str_weekly": "{0} недель",
|
||||
"calendar_rec_until_count2": "раз",
|
||||
"calendar_rec_until_count": "После",
|
||||
"calendar_str_daily": "{0} день",
|
||||
"calendar_rec_until_date": "На",
|
||||
"calendar_str_day": "на {0}",
|
||||
"calendar_rec_until_no": "Никогда",
|
||||
"calendar_rec_until": "Прекратить повторение",
|
||||
"calendar_str_monthday": "на {0}",
|
||||
"calendar_rec_freq_yearly": "годы",
|
||||
"calendar_str_nthdayofmonth": "на {0} день {1}",
|
||||
"calendar_str_for": "{0} раз",
|
||||
"calendar_rec_freq_monthly": "месяцы",
|
||||
"calendar_str_until": "до {0}",
|
||||
"calendar_rec_freq_weekly": "недели",
|
||||
"calendar_rec_freq_daily": "дни",
|
||||
"calendar_str_filter": "Фильтры:",
|
||||
"calendar_rec_txt": "Повторять каждый",
|
||||
"calendar_str_filter_month": "Месяцы: {0}",
|
||||
"calendar_str_filter_weekno": "Недели: {0}",
|
||||
"calendar_str_filter_yearday": "Дни года: {0}",
|
||||
"calendar_rec_custom": "Пользовательское",
|
||||
"calendar_str_filter_monthday": "Дни месяца: {0}",
|
||||
"calendar_rec_weekdays": "Ежедневно по будням",
|
||||
"calendar_str_filter_day": "Дни: {0}",
|
||||
"calendar_rec_edit": "Это повторяющееся событие",
|
||||
"calendar_rec_weekend": "Ежедневно по выходным",
|
||||
"calendar_rec_edit_one": "Редактировать только это событие",
|
||||
"calendar_rec_yearly": "Ежегодно по {2}",
|
||||
"calendar_rec_edit_from": "Редактировать будущие события",
|
||||
"calendar_rec_monthly": "Ежемесячно, день {1}",
|
||||
"calendar_rec_edit_all": "Редактировать все события",
|
||||
"calendar_rec_weekly": "Еженедельно по {0}",
|
||||
"calendar_rec_stop": "Прекратить повторение",
|
||||
"calendar_rec_daily": "Ежедневно",
|
||||
"calendar_rec_updated": "Правило обновлено {0}",
|
||||
"calendar_rec_no": "Один раз",
|
||||
"calendar_rec": "Повтор",
|
||||
"fm_rmFilter": "Удалить фильтр",
|
||||
"fm_filterBy": "Фильтр",
|
||||
"admin_conflictExplanation": "Существуют две версии этого документа. Восстановление архивной версии приведет к перезаписи текущей версии. Архивирование текущей версии приведет к перезаписи архивной версии. Ни одно из действий не может быть отменено.",
|
||||
"admin_documentConflict": "Архивировать/восстановить",
|
||||
"og_encryptedAppType": "Зашифровано {0}",
|
||||
"ui_jsRequired": "Для выполнения шифрования в вашем браузере должен быть включен JavaScript",
|
||||
"og_features": "{0} Возможности",
|
||||
"og_pricing": "{0} Цены",
|
||||
"og_contact": "{0} Контакт",
|
||||
"og_register": "Создать учетную запись на {0}",
|
||||
"og_login": "Войти в {0}",
|
||||
"og_default": "CryptPad: пакет для совместной работы со сквозным шифрованием",
|
||||
"og_teamDrive": "Диск команды",
|
||||
"admin_getRawMetadata": "История метаданных",
|
||||
"admin_planlimit": "Лимит хранилища",
|
||||
"admin_restoreDocument": "Восстановить документ из архива",
|
||||
"admin_archiveDocument": "Поместить документ в архив",
|
||||
"admin_restoreArchivedPins": "Восстановить журнал пин-кодов из архива",
|
||||
"admin_archivePinLog": "Архивировать лог пинов этой учетной записи",
|
||||
"admin_getPinList": "Текущий список пинов",
|
||||
"admin_restoreBlock": "Восстановить блок из архива",
|
||||
"admin_archiveBlock": "Архивировать блок",
|
||||
"admin_blockArchived": "Блок помещён в архив",
|
||||
"admin_blockAvailable": "Блок доступен",
|
||||
"admin_blockKey": "Публичный ключ блока",
|
||||
"admin_pinLogArchived": "Журнал пинов находится в архиве",
|
||||
"admin_pinLogAvailable": "Журнал пин-кодов доступен",
|
||||
"admin_fileCount": "Количество файлов",
|
||||
"admin_channelCount": "Количество документов",
|
||||
"admin_storageUsage": "Размер данных",
|
||||
"admin_note": "Примечание к тарифному плану",
|
||||
"admin_planName": "Название тарифного плана",
|
||||
"admin_currentlyOnline": "В настоящее время онлайн",
|
||||
"admin_lastPinTime": "Время активности второго PIN-а",
|
||||
"admin_firstPinTime": "Время активности первого PIN-а",
|
||||
"admin_accountMetadataPlaceholder": "Идентификатор пользователя (открытый ключ подписи)",
|
||||
"admin_blockMetadataPlaceholder": "Абсолютный или относительный URL-адрес блока",
|
||||
"admin_blockMetadataHint": "Блок информации о логине — это то, что позволяет войти в CryptPad под своей учётной записью с помощью комбинации имени пользователя и пароля",
|
||||
"admin_blockMetadataTitle": "Блок информации о логине",
|
||||
"admin_documentMetadataPlaceholder": "URL-адрес или идентификатор документа",
|
||||
"admin_channelArchived": "Архивировано",
|
||||
"admin_channelAvailable": "Доступно",
|
||||
"admin_currentlyOpen": "Сейчас открыто",
|
||||
"admin_documentModifiedTime": "Последнее изменение",
|
||||
"admin_documentCreationTime": "Создано",
|
||||
"admin_documentMetadata": "Метаданные на данный момент",
|
||||
"admin_documentSize": "Размер документа",
|
||||
"admin_documentMetadataHint": "Запросить документ или файл по его идентификатору или URL-адресу",
|
||||
"admin_documentMetadataTitle": "Информация о документе",
|
||||
"admin_accountMetadataHint": "Введите открытый ключ пользователя, чтобы получить данные об его учетной записи.",
|
||||
"admin_accountMetadataTitle": "Информация об учетной записи",
|
||||
"admin_restoreReason": "Пожалуйста, укажите причину восстановления и подтвердите, что Вы хотели бы продолжить",
|
||||
"admin_archiveReason": "Пожалуйста, укажите причину архивации и подтвердите, что Вы хотели бы продолжить",
|
||||
"ui_confirm": "Подтвердить",
|
||||
"ui_fetch": "Загрузить (fetch)",
|
||||
"ui_success": "Успешно",
|
||||
"ui_generateReport": "Создание отчета",
|
||||
"ui_none": "нет значения",
|
||||
"ui_false": "нет",
|
||||
"ui_true": "да",
|
||||
"admin_generatedAt": "Дата и время отчёта",
|
||||
"admin_cat_database": "База данных",
|
||||
"admin_uptimeHint": "Дата и время, в которое был запущен сервер",
|
||||
"admin_uptimeTitle": "Время запуска",
|
||||
"register_instance": "Создание новой учетной записи на {0}",
|
||||
"login_instance": "Подключитесь к своей учетной записи на {0}",
|
||||
"home_morestorage": "Чтобы получить больше места:",
|
||||
"home_location": "Зашифрованные данные размещены в {0}",
|
||||
"footer_website": "Веб-сайт проекта",
|
||||
"admin_noticeHint": "Необязательное сообщение для отображения на главной странице",
|
||||
"admin_noticeTitle": "Уведомление на домашней странице",
|
||||
"ui_experimental": "Эта функция считается экспериментальной.",
|
||||
"error_evalPermitted": "Прервано, потому что eval не должно быть разрешено.\n\nЭта ошибка связана с заголовками политики безопасности содержимого (Content-Security-Policy headers), это может быть связано с: устаревшим браузером, который их не поддерживает, расширениями браузера, которые мешают их правильному поведению, или неправильной конфигурацией этого экземпляра CryptPad.",
|
||||
"error_incorrectAccess": "Доступ к этой странице возможен только через {0}.",
|
||||
"error_embeddingDisabledSpecific": "Встраивание отключено для этого приложения CryptPad.",
|
||||
"error_embeddingDisabled": "Встраивание отключено для этого экземпляра CryptPad",
|
||||
"admin_enableembedsHint": "Разрешить встраивать документы и носители из этого экземпляра на другие веб-сайты. Это добавит опцию 'Встроить' в меню 'Поделиться'. По соображениям безопасности приложения, использующие OnlyOffice (Листы, Документ, Презентация), не могут быть встроены, даже если этот параметр активен.",
|
||||
"admin_enableembedsTitle": "Включить удаленное встраивание",
|
||||
"ui_ms": "миллисекунд",
|
||||
"admin_setDuration": "Установить продолжительность",
|
||||
"admin_bytesWrittenHint": "Если Вы включили измерение производительности диска, то продолжительность окна можно настроить ниже.",
|
||||
"admin_bytesWrittenTitle": "Окно измерения производительности диска",
|
||||
"admin_enableDiskMeasurementsHint": "Если включено, эндпойнт JSON API будет доступен в разделе <code>/api/profiling</code>. Это позволяет поддерживать текущее измерение дискового ввода-вывода в пределах временного окна, установленного ниже. Этот параметр может повлиять на производительность сервера и может привести к раскрытию конфиденциальных данных. Рекомендуется оставить этот параметр отключенным, если только Вы не знаете, что делаете.",
|
||||
"admin_enableDiskMeasurementsTitle": "Измерение производительности диска",
|
||||
"admin_infoNotice2": "Подробности смотрите на вкладке 'Сеть'.",
|
||||
"admin_infoNotice1": "Используйте следующие поля для описания Вашего экземпляра. Эта информация используется на главной странице экземпляра. Она также отправляется как часть телеметрии сервера, если Вы хотите быть включенным в список общедоступных экземпляров CryptPad.",
|
||||
"admin_reviewCheckupNotice": "Рекомендуется просмотреть страницу <a>проверки</a>, чтобы убедиться, что этот экземпляр настроен правильно.",
|
||||
"admin_cacheEvictionRequired": "Сервер был обновлен с учетом новых настроек. Пожалуйста, используйте кнопку <b>Очистить кэш</b>, чтобы убедиться, что это изменение станет видимым для всех пользователей.",
|
||||
"fivehundred_internalServerError": "Внутренняя ошибка сервера",
|
||||
"support_debuggingDataHint": "Следующая информация будет включена в отправленные Вами обращения в службу поддержки. Ничто из этого не позволяет администраторам получать доступ к Вашим документам или расшифровывать их. Эта информация зашифрована таким образом, что только администраторы могут ее прочитать.",
|
||||
"support_debuggingDataTitle": "Отладочные данные учётной записи",
|
||||
"support_cat_debugging": "Отладочные данные",
|
||||
"ui_openDirectly": "Эта функция недоступна когда (документ) CryptPad встроен в другой сайт. Открыть этот документ на отдельной вкладке браузера?",
|
||||
"support_cat_abuse": "Нарушены Условия Обслуживания",
|
||||
"support_cat_document": "Документ",
|
||||
"support_cat_drives": "Хранилище (Drive) или Команда",
|
||||
"support_warning_other": "О чём Ваш запрос? Пожалуйста, предоставьте как можно больше актуальной информации, чтобы нам было легче решить вашу проблему быстро",
|
||||
"support_warning_abuse": "Пожалуйста, сообщайте о контенте, который нарушает <a>Условия Обслуживания</a>. Пожалуйста, предоставьте ссылки на оскорбительные документы или профили пользователей и опишите, как они нарушают условия. Любая дополнительная информация о контексте, в котором Вы обнаружили контент или поведение, может помочь администраторам предотвратить будущие нарушения",
|
||||
"support_warning_bug": "Пожалуйста, укажите, в каком браузере возникает проблема и установлены ли какие-либо расширения. Пожалуйста, предоставьте как можно больше подробностей о проблеме и шагах, необходимых для ее воспроизведения",
|
||||
"support_warning_document": "Пожалуйста, укажите, какой тип документа вызывает проблему, и укажите <a>идентификатор документа</a> или ссылку на документ",
|
||||
"support_warning_drives": "Обратите внимание, что у администраторов нет возможности находить папки и документы по имени. Для общих папок, пожалуйста, укажите <a>идентификатор документа</a>",
|
||||
"support_warning_account": "Пожалуйста, обратите внимание, что администраторы не могут сменить Ваш пароль. Если Вы потеряли учетные данные для своей учетной записи, но всё ещё авторизованы в системе, Вы можете <a>перенести свои данные в новую учетную запись</a>",
|
||||
"support_warning_prompt": "Пожалуйста, выберите наиболее подходящую категорию для вашего вопроса. Это помогает администраторам определять срочность и сложность проблемы, и дает дополнительные рекомендации относительно того, какую информацию следует предоставлять",
|
||||
"info_sourceFlavour": "<a>Исходный код</a> CryptPad",
|
||||
"info_termsFlavour": "<a>Условия обслуживания</a> в этом экземпляре CryptPad",
|
||||
"footer_source": "Исходный код",
|
||||
"admin_jurisdictionHint": "Страна, в которой размещены зашифрованные данные этого экземпляра",
|
||||
"admin_jurisdictionTitle": "Местоположение хостинга",
|
||||
"admin_descriptionHint": "Текстовое описание, отображаемое для этого экземпляра в списке общедоступных экземпляров на cryptpad.org",
|
||||
"admin_descriptionTitle": "Описание экземпляра CryptPad",
|
||||
"ui_saved": "{0} сохранено",
|
||||
"admin_nameHint": "Имя, отображаемое для этого экземпляра в списке общедоступных экземпляров на cryptpad.org",
|
||||
"admin_nameTitle": "Имя экземпляря CryptPad",
|
||||
"admin_archiveNote": "Заметка",
|
||||
"form_editable_on": "Один раз и отредактировать"
|
||||
}
|
||||
|
|
|
@ -276,7 +276,7 @@
|
|||
"contacts_confirmRemove": "确定要从联系人中删除 <em>{0}</em> 吗?",
|
||||
"contacts_remove": "删除此联系人",
|
||||
"contacts_send": "发送",
|
||||
"contacts_request": "<em>{0}</em> 想将您添加为联系人。 <b>接受</b>?",
|
||||
"contacts_request": "<em>{0}</em> 想将您添加为联系人。 <b>要接受吗</b>?",
|
||||
"contacts_rejected": "联系人邀请被拒绝",
|
||||
"contacts_added": "联系人已接受邀请。",
|
||||
"contacts_title": "联系人",
|
||||
|
@ -1414,7 +1414,7 @@
|
|||
"admin_enableDiskMeasurementsHint": "如果启用,JSON API 端点将在 <code>/api/profiling</code> 下公开。 这会在下面设置的时间窗口内保持磁盘 I/O 的运行测量。 此设置可能会影响服务器性能并可能泄露敏感数据。 除非您知道自己在做什幺,否则建议您禁用此设置。",
|
||||
"admin_enableDiskMeasurementsTitle": "测量磁盘性能",
|
||||
"admin_infoNotice2": "有关详细信息,请参阅“网络”选项卡。",
|
||||
"admin_infoNotice1": "使用以下字段来描述您的实例。 此信息将用于 CryptPad 未来版本的实例首页。 仅当您选择加入公共 CryptPad 实例列表时,它才会作为服务器遥测的一部分发送。",
|
||||
"admin_infoNotice1": "使用以下字段来描述您的实例。 此信息用于 CryptPad 的实例前端页。 如果您选择加入公共 CryptPad 实例列表,它会同时作为服务器遥测的一部分信息发送。",
|
||||
"admin_reviewCheckupNotice": "建议查看 <a>checkup</a> 页面以确认此实例配置正确。",
|
||||
"admin_cacheEvictionRequired": "服务器已使用新设置进行了更新。 请使用 <b>刷新缓存</b> 按钮确保此更改对所有用户可见。",
|
||||
"fivehundred_internalServerError": "内部服务器错误",
|
||||
|
@ -1437,5 +1437,9 @@
|
|||
"ui_restore": "恢复",
|
||||
"ui_archive": "归档",
|
||||
"ui_undefined": "未知",
|
||||
"admin_documentType": "類型"
|
||||
"admin_documentType": "類型",
|
||||
"admin_generatedAt": "报告时间戳",
|
||||
"admin_cat_database": "数据库",
|
||||
"admin_uptimeHint": "启动服务器的日期和时间",
|
||||
"admin_uptimeTitle": "启动时间"
|
||||
}
|
||||
|
|
|
@ -293,7 +293,7 @@ define([
|
|||
};
|
||||
|
||||
Messages.convertPage = "Convert"; // XXX 4.11.0
|
||||
Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward."; // XXX 4.11.0
|
||||
Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterwards."; // XXX 4.11.0
|
||||
|
||||
var createToolbar = function () {
|
||||
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
|
||||
|
|
|
@ -193,28 +193,20 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
.cp-form-settings-preview {
|
||||
min-width: 260px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.cp-form-color-theme-container {
|
||||
max-width: 300px;
|
||||
}
|
||||
& > div {
|
||||
flex-basis: 33.333333%;
|
||||
flex: 1;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) and (min-width: 400px) {
|
||||
.cp-form-creator-settings {
|
||||
& > div {
|
||||
flex-basis: 50% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 400px) {
|
||||
.cp-form-creator-settings {
|
||||
& > div {
|
||||
flex-basis: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cp-form-creator-settings {
|
||||
.cp-form-actions {
|
||||
margin-top: 5px;
|
||||
|
@ -230,6 +222,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
div.cp-form-settings-preview {
|
||||
background: @cp_form-bg1;
|
||||
padding: 10px;
|
||||
border-radius: @variables_radius_L;
|
||||
& > *:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cp-form-setting-title {
|
||||
color: @cryptpad_color_link;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-form-color-container {
|
||||
& > div {
|
||||
display: flex;
|
||||
|
@ -251,12 +255,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cp-form-response-msg-container button {
|
||||
white-space: initial;
|
||||
line-height: 25px;
|
||||
padding: 5.5px 6px;
|
||||
}
|
||||
}
|
||||
div.cp-form-filler-container {
|
||||
width: 300px;
|
||||
|
@ -276,6 +274,7 @@
|
|||
}
|
||||
}
|
||||
div.cp-form-creator-content {
|
||||
position: relative;
|
||||
.cp-form-block-type {
|
||||
margin-top: -35px;
|
||||
&.editable {
|
||||
|
@ -404,7 +403,7 @@
|
|||
display: flex;
|
||||
flex-flow: column;
|
||||
margin: 20px 0px 0px 0px;
|
||||
padding-bottom: 100px;
|
||||
padding-bottom: 75px;
|
||||
&> div:first-child {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
@ -449,6 +448,17 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.cp-form-response-msg-container {
|
||||
padding-bottom: 75px;
|
||||
button {
|
||||
white-space: initial;
|
||||
line-height: 25px;
|
||||
padding: 5.5px 6px;
|
||||
}
|
||||
.cp-form-response-msg-hint {
|
||||
color: @cryptpad_color_link;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-form-send-container {
|
||||
text-align: center;
|
||||
|
@ -666,7 +676,7 @@
|
|||
}
|
||||
}
|
||||
&.editable {
|
||||
cursor: grab;
|
||||
&:not(.nodrag) { cursor: grab; }
|
||||
.cp-form-edit-save {
|
||||
margin-top: 20px;
|
||||
button {
|
||||
|
@ -764,8 +774,13 @@
|
|||
justify-content: center;
|
||||
flex: 1;
|
||||
flex-flow: column;
|
||||
div.cp-form-submit-table {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.cp-form-submit-actions {
|
||||
button:not(:last-child) {
|
||||
span:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -833,6 +848,10 @@
|
|||
background: @cp_form-bg2;
|
||||
}
|
||||
|
||||
.cp-form-results-delete {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.cp-form-results-type-multiradio-data {
|
||||
.cp-mr-q {
|
||||
font-weight: bold;
|
||||
|
@ -915,11 +934,27 @@
|
|||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: baseline;
|
||||
.cp-radio {
|
||||
.cp-radio, .cp-checkmark {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
.cp-checkmark-label {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
.cp-form-multiradio-container {
|
||||
overflow: auto;
|
||||
}
|
||||
.cp-form-type-multiradio {
|
||||
.cp-form-multiradio-header {
|
||||
white-space: nowrap;
|
||||
span {
|
||||
text-align: center;
|
||||
max-width: 30ch;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
display: table;
|
||||
& > * {
|
||||
display: table-row;
|
||||
|
@ -928,9 +963,25 @@
|
|||
padding: 5px 20px;
|
||||
vertical-align: middle;
|
||||
&:first-child {
|
||||
min-width: 200px;
|
||||
//overflow: auto;
|
||||
max-width: 300px;
|
||||
min-width: 300px;
|
||||
width: 300px;
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
word-break: break-word;
|
||||
background: @cp_form-bg1;
|
||||
z-index: 2;
|
||||
}
|
||||
.cp-radio-mark {
|
||||
&:nth-child(2) {
|
||||
visibility: hidden;
|
||||
//overflow: auto;
|
||||
max-width: 300px;
|
||||
min-width: 300px;
|
||||
width: 300px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.cp-radio-mark, .cp-checkmark-mark {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
@ -942,8 +993,11 @@
|
|||
.cp-form-type-sort {
|
||||
cursor: grab;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
word-break: break-word;
|
||||
.cp-form-handle {
|
||||
margin-right: 5px;
|
||||
min-width: 12px;
|
||||
}
|
||||
.cp-form-sort-order {
|
||||
border: 1px solid @cryptpad_text_col;
|
||||
|
@ -962,22 +1016,24 @@
|
|||
.cp-form-type-poll-container {
|
||||
overflow: auto;
|
||||
.cp-form-poll-hint {
|
||||
margin-bottom: 10px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
.cp-form-type-poll {
|
||||
margin-top: 32px; // cp-form-poll-hint is "absolute"
|
||||
display: inline-flex;
|
||||
flex-flow: column;
|
||||
width: 100%;
|
||||
& > div {
|
||||
display: flex;
|
||||
}
|
||||
.cp-poll-total {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.cp-form-poll-body {
|
||||
flex-flow: column;
|
||||
max-height: 225px;
|
||||
overflow: auto;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
|
@ -985,15 +1041,15 @@
|
|||
}
|
||||
.cp-poll-cell {
|
||||
width: 100px;
|
||||
height: 35px;
|
||||
height: 40px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top:5px;
|
||||
margin-left:5px;
|
||||
&:first-child {
|
||||
width: 200px;
|
||||
}
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
word-break: break-word;
|
||||
button {
|
||||
width: 100%;
|
||||
border-top: 0px;
|
||||
|
@ -1039,6 +1095,27 @@
|
|||
.cp-poll-time-day {
|
||||
flex-basis: 100px;
|
||||
border-bottom: 1px solid @cryptpad_text_col;
|
||||
&:first-child {
|
||||
border-color: @cp_form-bg1 !important;
|
||||
}
|
||||
}
|
||||
.cp-form-poll-option, .cp-poll-time-day {
|
||||
span {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
line-height: 1.2;
|
||||
hyphens: auto;
|
||||
text-align: center;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
.cp-poll-switch {
|
||||
button.btn {
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
&:not(.cp-form-poll-switch) {
|
||||
& > div {
|
||||
|
@ -1046,13 +1123,25 @@
|
|||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
.cp-form-poll-body {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
||||
.cp-poll-cell {
|
||||
&:first-child {
|
||||
min-width: 200px;
|
||||
background: #424242;
|
||||
position: absolute;
|
||||
margin-left: 0;
|
||||
background: @cp_form-bg1;
|
||||
}
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
&:nth-child(2) {
|
||||
margin-left: 205px;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-form-poll-option, .cp-poll-time-day, .cp-form-poll-choice {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cp-form-poll-option, .cp-poll-time-day {
|
||||
flex-flow: column;
|
||||
text-align: center;
|
||||
|
@ -1074,9 +1163,6 @@
|
|||
flex-flow: column;
|
||||
&.cp-form-poll-body {
|
||||
flex-flow: row;
|
||||
max-width: 550px;
|
||||
max-height: unset;
|
||||
scroll-snap-type: x mandatory;
|
||||
& > div {
|
||||
flex-flow: column;
|
||||
}
|
||||
|
@ -1091,12 +1177,18 @@
|
|||
}
|
||||
}
|
||||
.cp-form-poll-option, .cp-poll-switch {
|
||||
span {
|
||||
max-height: 100%;
|
||||
}
|
||||
width: 200px;
|
||||
.cp-form-weekday-separator {
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
div.cp-poll-time-day-container {
|
||||
width: auto !important;
|
||||
}
|
||||
.cp-poll-time-day {
|
||||
flex-basis: 40px;
|
||||
border-right: none;
|
||||
|
@ -1145,6 +1237,34 @@
|
|||
}
|
||||
}
|
||||
|
||||
#cp-form-settings {
|
||||
.cp-modal {
|
||||
text-align: left;
|
||||
width: 500px;
|
||||
padding: 24px;
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
line-height: 40px;
|
||||
margin-top: -12px;
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
& > *:not(h2) {
|
||||
color: @cryptpad_text_col;
|
||||
}
|
||||
& > div:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cp-form-setting-title {
|
||||
color: @cryptpad_color_link;
|
||||
}
|
||||
}
|
||||
}
|
||||
& > .flatpickr-calendar {
|
||||
z-index: 100001;
|
||||
}
|
||||
|
||||
.charts_main();
|
||||
}
|
||||
|
||||
|
|
|
@ -13,8 +13,74 @@ define([
|
|||
value += '"' + vv + '"';
|
||||
return value;
|
||||
};
|
||||
Export.results = function (content, answers, TYPES, order, isArray) {
|
||||
|
||||
var exportJSON = function (content, answers, TYPES, order) {
|
||||
var form = content.form;
|
||||
var res = {
|
||||
questions: {},
|
||||
responses: []
|
||||
};
|
||||
var q = res.questions;
|
||||
var r = res.responses;
|
||||
|
||||
// Add questions
|
||||
var i = 1;
|
||||
order.forEach(function (key) {
|
||||
var obj = form[key];
|
||||
if (!obj) { return; }
|
||||
var type = obj.type;
|
||||
if (!TYPES[type]) { return; } // Ignore static types
|
||||
var id = `q${i++}`;
|
||||
if (TYPES[type] && TYPES[type].exportCSV) {
|
||||
var _obj = Util.clone(obj);
|
||||
_obj.q = "tmp";
|
||||
q[id] = {
|
||||
question: obj.q,
|
||||
items: TYPES[type].exportCSV(false, _obj).map(function (str) {
|
||||
return str.slice(6); // Remove "tmp | "
|
||||
})
|
||||
};
|
||||
} else {
|
||||
q[id] = obj.q || Messages.form_default;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(answers || {}).forEach(function (key) {
|
||||
var userObj = answers[key];
|
||||
Object.keys(userObj).forEach(function (k) {
|
||||
var obj = userObj[k];
|
||||
var time = new Date(obj.time).toISOString();
|
||||
var msg = obj.msg || {};
|
||||
var user = msg._userdata || {};
|
||||
var data = {
|
||||
'_time': time,
|
||||
'_name': user.name || Messages.anonymous
|
||||
};
|
||||
|
||||
var i = 1;
|
||||
order.forEach(function (key) {
|
||||
if (!form[key]) { return; }
|
||||
var type = form[key].type;
|
||||
if (!TYPES[type]) { return; } // Ignore static types
|
||||
var id = `q${i++}`;
|
||||
if (TYPES[type].exportCSV) {
|
||||
data[id] = TYPES[type].exportCSV(msg[key], form[key]);
|
||||
return;
|
||||
}
|
||||
data[id] = msg[key];
|
||||
});
|
||||
r.push(data);
|
||||
});
|
||||
});
|
||||
|
||||
return JSON.stringify(res, 0, 2);
|
||||
};
|
||||
Export.results = function (content, answers, TYPES, order, format) {
|
||||
if (!content || !content.form) { return; }
|
||||
|
||||
if (format === "json") { return exportJSON(content, answers, TYPES, order); }
|
||||
|
||||
var isArray = format === "array";
|
||||
var csv = "";
|
||||
var array = [];
|
||||
var form = content.form;
|
||||
|
@ -39,7 +105,9 @@ define([
|
|||
array.push(questions);
|
||||
|
||||
Object.keys(answers || {}).forEach(function (key) {
|
||||
var obj = answers[key];
|
||||
var _obj = answers[key];
|
||||
Object.keys(_obj).forEach(function (uid) {
|
||||
var obj = _obj[uid];
|
||||
csv += '\n';
|
||||
var time = new Date(obj.time).toISOString();
|
||||
var msg = obj.msg || {};
|
||||
|
@ -63,6 +131,7 @@ define([
|
|||
});
|
||||
array.push(line);
|
||||
});
|
||||
});
|
||||
if (isArray) { return array; }
|
||||
return csv;
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
122
www/form/main.js
122
www/form/main.js
|
@ -44,6 +44,7 @@ define([
|
|||
var addRpc = function (sframeChan, Cryptpad, Utils) {
|
||||
sframeChan.on('EV_FORM_PIN', function (data) {
|
||||
channels.answersChannel = data.channel;
|
||||
Cryptpad.changeMetadata();
|
||||
Cryptpad.getPadAttribute('answersChannel', function (err, res) {
|
||||
// If already stored, don't pin it again
|
||||
if (res && res === data.channel) { return; }
|
||||
|
@ -122,6 +123,8 @@ define([
|
|||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
var deleteLines = false; // "false" to support old forms
|
||||
sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) {
|
||||
var cb = Utils.Util.once(_cb);
|
||||
var myKeys = {};
|
||||
|
@ -138,12 +141,13 @@ define([
|
|||
CPNetflux = _CPNetflux;
|
||||
Pinpad = _Pinpad;
|
||||
}));
|
||||
var personalDrive = !Cryptpad.initialTeam || Cryptpad.initialTeam === -1;
|
||||
Cryptpad.getAccessKeys(w(function (_keys) {
|
||||
if (!Array.isArray(_keys)) { return; }
|
||||
accessKeys = _keys;
|
||||
|
||||
_keys.some(function (_k) {
|
||||
if ((!Cryptpad.initialTeam && !_k.id) || Cryptpad.initialTeam === _k.id) {
|
||||
if ((personalDrive && !_k.id) || Cryptpad.initialTeam === Number(_k.id)) {
|
||||
myKeys = _k;
|
||||
return true;
|
||||
}
|
||||
|
@ -160,6 +164,9 @@ define([
|
|||
Cryptpad.makeNetwork(w(function (err, nw) {
|
||||
network = nw;
|
||||
}));
|
||||
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
|
||||
if (md && md.deleteLines) { deleteLines = true; }
|
||||
}));
|
||||
}).nThen(function () {
|
||||
if (!network) { return void cb({error: "E_CONNECT"}); }
|
||||
|
||||
|
@ -183,6 +190,9 @@ define([
|
|||
validateKey: keys.secondaryValidateKey,
|
||||
owners: [myKeys.edPublic],
|
||||
crypto: crypto,
|
||||
metadata: {
|
||||
deleteLines: true
|
||||
}
|
||||
//Cache: Utils.Cache // TODO enable cache for form responses when the cache stops evicting old answers
|
||||
};
|
||||
var results = {};
|
||||
|
@ -198,7 +208,6 @@ define([
|
|||
nThen(function (waitFor) {
|
||||
accessKeys.forEach(function (obj) {
|
||||
Pinpad.create(network, obj, waitFor(function (e) {
|
||||
console.log('done', obj);
|
||||
if (e) { console.error(e); }
|
||||
}));
|
||||
});
|
||||
|
@ -222,14 +231,26 @@ define([
|
|||
config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) {
|
||||
var parsed = Utils.Util.tryParse(msg);
|
||||
if (!parsed) { return; }
|
||||
var uid = parsed._uid || '000';
|
||||
|
||||
// If we have a "non-anonymous" answer, it may be the edition of a
|
||||
// previous anonymous answer. Check if a previous anonymous answer exists
|
||||
// with the same uid and delete it.
|
||||
if (parsed._proof) {
|
||||
var check = checkAnonProof(parsed._proof, data.channel, curvePrivate);
|
||||
if (check) {
|
||||
delete results[parsed._proof.key];
|
||||
var theirAnonKey = parsed._proof.key;
|
||||
if (check && results[theirAnonKey] && results[theirAnonKey][uid]) {
|
||||
delete results[theirAnonKey][uid];
|
||||
}
|
||||
}
|
||||
if (data.cantEdit && results[senderCurve]) { return; }
|
||||
results[senderCurve] = {
|
||||
|
||||
parsed._time = cfg && cfg.time;
|
||||
if (deleteLines) { parsed._hash = hash; }
|
||||
|
||||
if (data.cantEdit && results[senderCurve]
|
||||
&& results[senderCurve][uid]) { return; }
|
||||
results[senderCurve] = results[senderCurve] || {};
|
||||
results[senderCurve][uid] = {
|
||||
msg: parsed,
|
||||
hash: hash,
|
||||
time: cfg && cfg.time
|
||||
|
@ -239,7 +260,7 @@ define([
|
|||
});
|
||||
});
|
||||
sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) {
|
||||
var answer;
|
||||
var answers = [];
|
||||
var myKeys;
|
||||
nThen(function (w) {
|
||||
Cryptpad.getFormKeys(w(function (keys) {
|
||||
|
@ -259,38 +280,68 @@ define([
|
|||
w.abort();
|
||||
return void cb(obj);
|
||||
}
|
||||
answer = obj;
|
||||
// Get the latest edit per uid
|
||||
var temp = {};
|
||||
obj.forEach(function (ans) {
|
||||
var uid = ans.uid || '000';
|
||||
temp[uid] = ans;
|
||||
});
|
||||
answers = Object.values(temp);
|
||||
}));
|
||||
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
|
||||
if (md && md.deleteLines) { deleteLines = true; }
|
||||
}));
|
||||
}).nThen(function () {
|
||||
var n = nThen;
|
||||
var err;
|
||||
var all = {};
|
||||
answers.forEach(function (answer) {
|
||||
n = n(function(waitFor) {
|
||||
var finalKeys = myKeys;
|
||||
if (answer.anonymous) {
|
||||
if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
|
||||
myKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
|
||||
if (!myKeys.formSeed) {
|
||||
err = 'ANONYMOUS_ERROR';
|
||||
console.error('ANONYMOUS_ERROR', answer);
|
||||
return;
|
||||
}
|
||||
finalKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
|
||||
}
|
||||
Cryptpad.getHistoryRange({
|
||||
channel: data.channel,
|
||||
lastKnownHash: answer.hash,
|
||||
toHash: answer.hash,
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) { return void cb(obj); }
|
||||
}, waitFor(function (obj) {
|
||||
if (obj && obj.error) { err = obj.error; return; }
|
||||
var messages = obj.messages;
|
||||
if (!messages.length) { return void cb(); }
|
||||
if (obj.lastKnownHash !== answer.hash) { return void cb(); }
|
||||
if (!messages.length) {
|
||||
// XXX TODO delete from drive.forms
|
||||
return;
|
||||
}
|
||||
if (obj.lastKnownHash !== answer.hash) { return; }
|
||||
try {
|
||||
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
|
||||
validateKey: data.validateKey,
|
||||
ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
|
||||
my_private: Nacl.util.decodeBase64(myKeys.curvePrivate),
|
||||
my_private: Nacl.util.decodeBase64(finalKeys.curvePrivate),
|
||||
their_public: Nacl.util.decodeBase64(data.publicKey)
|
||||
});
|
||||
var parsed = JSON.parse(res.content);
|
||||
parsed._isAnon = answer.anonymous;
|
||||
parsed._time = messages[0].time;
|
||||
cb(parsed);
|
||||
if (deleteLines) { parsed._hash = answer.hash; }
|
||||
var uid = parsed._uid || '000';
|
||||
if (all[uid] && !all[uid]._isAnon) { parsed._isAnon = false; }
|
||||
all[uid] = parsed;
|
||||
} catch (e) {
|
||||
cb({error: e});
|
||||
err = e;
|
||||
}
|
||||
}));
|
||||
}).nThen;
|
||||
});
|
||||
n(function () {
|
||||
if (err) { return void cb({error: err}); }
|
||||
cb(all);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -304,11 +355,6 @@ define([
|
|||
// We can create a seed in localStorage.
|
||||
if (!keys.formSeed) {
|
||||
// No drive mode
|
||||
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
|
||||
if(answered.indexOf(data.channel) !== -1) {
|
||||
// Already answered: abort
|
||||
return void cb({ error: "EANSWERED" });
|
||||
}
|
||||
keys = { formSeed: noDriveSeed };
|
||||
}
|
||||
myKeys = keys;
|
||||
|
@ -334,6 +380,8 @@ define([
|
|||
}
|
||||
|
||||
var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys);
|
||||
var uid = data.results._uid || Utils.Util.uid();
|
||||
data.results._uid = uid;
|
||||
var text = JSON.stringify(data.results);
|
||||
var ciphertext = crypto.encrypt(text, box.publicKey);
|
||||
|
||||
|
@ -343,15 +391,39 @@ define([
|
|||
ciphertext
|
||||
], function (err, response) {
|
||||
Cryptpad.storeFormAnswer({
|
||||
uid: uid,
|
||||
channel: box.channel,
|
||||
hash: hash,
|
||||
curvePrivate: ephemeral_private,
|
||||
anonymous: Boolean(data.anonymous)
|
||||
});
|
||||
cb({error: err, response: response, hash: hash});
|
||||
}, function () {
|
||||
var res = data.results;
|
||||
res._isAnon = data.anonymous;
|
||||
res._time = +new Date();
|
||||
if (deleteLines) { res._hash = hash; }
|
||||
cb({
|
||||
error: err,
|
||||
response: response,
|
||||
results: res
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on("Q_FORM_DELETE_ALL_ANSWERS", function (data, cb) {
|
||||
if (!data || !data.channel) { return void cb({error: 'EINVAL'}); }
|
||||
Cryptpad.clearOwnedChannel(data, cb);
|
||||
});
|
||||
sframeChan.on("Q_FORM_DELETE_ANSWER", function (data, cb) {
|
||||
if (!deleteLines) {
|
||||
return void cb({error: 'EFORBIDDEN'});
|
||||
}
|
||||
Cryptpad.deleteFormAnswers(data, cb);
|
||||
});
|
||||
sframeChan.on("Q_FORM_MUTE", function (data, cb) {
|
||||
if (!Utils.secret) { return void cb({error: 'EINVAL'}); }
|
||||
Cryptpad.muteChannel(Utils.secret.channel, data.muted, cb);
|
||||
});
|
||||
};
|
||||
SFCommonO.start({
|
||||
addData: addData,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -43,7 +43,14 @@ define([
|
|||
};
|
||||
var getEndDate = function () {
|
||||
setTimeout(function () { $(endPickr.calendarContainer).remove(); });
|
||||
return endPickr.parseDate(e.value);
|
||||
var d = endPickr.parseDate(e.value);
|
||||
|
||||
if (endPickr.config.dateFormat === "Y-m-d") { // All day event
|
||||
// Tui-calendar will remove 1s (1000ms) to the date for an unknown reason...
|
||||
d.setMilliseconds(1000);
|
||||
}
|
||||
|
||||
return d;
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -12,4 +12,5 @@ This file is intended to be used as a log of what third-party source we have ven
|
|||
* [Fabricjs 4.6.0](https://github.com/fabricjs/fabric.js) and [Fabric-history](https://github.com/lyzerk/fabric-history) for the whiteboard app
|
||||
* [Requirejs optional module plugin](https://stackoverflow.com/a/27422370)
|
||||
* [asciidoc.js 2.0.0](https://github.com/asciidoctor/codemirror-asciidoc/releases/tag/2.0.0) with slight changes to match the format of other codemirror modes
|
||||
* [Asciidoctor.js 2.2.6](https://github.com/asciidoctor/asciidoctor.js/releases/tag/v2.2.6) for AsciiDoc rendering
|
||||
|
||||
|
|
|
@ -1704,7 +1704,7 @@ define([
|
|||
drive: 'fa fa-hdd-o',
|
||||
cursor: 'fa fa-i-cursor',
|
||||
code: 'fa fa-file-code-o',
|
||||
pad: 'fa fa-file-word-o',
|
||||
pad: 'cptools cptools-richtext',
|
||||
security: 'fa fa-lock',
|
||||
subscription: 'fa fa-star-o',
|
||||
kanban: 'cptools cptools-kanban',
|
||||
|
|
|
@ -285,6 +285,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
.cp-teams-invite-uses { // XXX
|
||||
input {
|
||||
margin-bottom: 0px !important;
|
||||
margin-right: 10px;
|
||||
width: 75px !important;
|
||||
}
|
||||
|
||||
}
|
||||
.cp-teams-invite-role {
|
||||
margin-bottom: 15px;
|
||||
span:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#cp-teams-roster-dialog {
|
||||
table {
|
||||
width: 100%;
|
||||
|
|
|
@ -17,6 +17,7 @@ define([
|
|||
'/customize/application_config.js',
|
||||
'/common/messenger-ui.js',
|
||||
'/common/inner/invitation.js',
|
||||
'/common/clipboard.js',
|
||||
'/common/make-backup.js',
|
||||
'/customize/messages.js',
|
||||
|
||||
|
@ -43,6 +44,7 @@ define([
|
|||
AppConfig,
|
||||
MessengerUI,
|
||||
InviteInner,
|
||||
Clipboard,
|
||||
Backup,
|
||||
Messages)
|
||||
{
|
||||
|
@ -714,6 +716,18 @@ define([
|
|||
title: Messages.team_pendingOwnerTitle
|
||||
}, ' ' + Messages.team_pendingOwner));
|
||||
}
|
||||
if (data.pending && data.inviteChannel && data.remaining === -1) { // Invite link
|
||||
$(name).append(h('em', ' ' + Messages.team_linkUsesInfinite));
|
||||
} else if (data.pending && data.inviteChannel) {
|
||||
$(name).append(h('em', ' ' + Messages._getKey('team_linkUses', [
|
||||
data.remaining || 1,
|
||||
data.totalUses || 1
|
||||
])));
|
||||
}
|
||||
if (data.pending && data.inviteChannel) {
|
||||
var r = data.role === "MEMBER" ? Messages.team_members : Messages.team_viewers;
|
||||
$(name).append(h('em', ' (' + r + ')'));
|
||||
}
|
||||
// Status
|
||||
var status = h('span.cp-team-member-status'+(data.online ? '.online' : ''));
|
||||
// Actions
|
||||
|
@ -817,6 +831,23 @@ define([
|
|||
actions,
|
||||
status,
|
||||
];
|
||||
if (data.inviteChannel) {
|
||||
if (data.hash) {
|
||||
var copy = h('span.fa.fa-copy');
|
||||
$(copy).click(function () {
|
||||
var privateData = common.getMetadataMgr().getPrivateData();
|
||||
var origin = privateData.origin;
|
||||
var href = origin + Hash.hashToHref(data.hash, 'teams');
|
||||
var success = Clipboard.copy(href);
|
||||
if (success) { UI.log(Messages.shareSuccess); }
|
||||
}).prependTo(actions);
|
||||
}
|
||||
content = [
|
||||
avatar,
|
||||
name,
|
||||
actions
|
||||
];
|
||||
}
|
||||
var div = h('div.cp-team-roster-member', content);
|
||||
if (data.profile) {
|
||||
$(div).dblclick(function (e) {
|
||||
|
@ -872,7 +903,7 @@ define([
|
|||
if (!roster[k].pending) { return; }
|
||||
if (!roster[k].inviteChannel) { return; }
|
||||
roster[k].curvePublic = k;
|
||||
return roster[k].role === "VIEWER" || !roster[k].role;
|
||||
return roster[k].role === "MEMBER" || roster[k].role === "VIEWER" || !roster[k].role;
|
||||
}).map(function (k) {
|
||||
return makeMember(common, roster[k], me);
|
||||
});
|
||||
|
@ -1349,7 +1380,7 @@ define([
|
|||
Messages._getKey('team_inviteFromMsg',
|
||||
[Util.fixHTML(getDisplayName(json.author.displayName)),
|
||||
Util.fixHTML(json.teamName)])));
|
||||
if (typeof(json.message) === 'string') {
|
||||
if (typeof(json.message) === 'string' && json.message) {
|
||||
var message = h('div.cp-teams-invite-message');
|
||||
json.message.split('\n').forEach(line => {
|
||||
if (line.trim()) {
|
||||
|
@ -1524,12 +1555,12 @@ define([
|
|||
$div.empty().append(content);
|
||||
});
|
||||
}
|
||||
var $divLink = $('div.cp-team-link').empty();
|
||||
/*var $divLink = $('div.cp-team-link').empty();
|
||||
if ($divLink.length) {
|
||||
refreshLink(common, function (content) {
|
||||
$divLink.append(content);
|
||||
});
|
||||
}
|
||||
}*/
|
||||
var $divCreate = $('div.cp-team-create');
|
||||
if ($divCreate.length) {
|
||||
refreshCreate(common, function (content) {
|
||||
|
|
Loading…
Reference in New Issue