mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-12 19:49:59 +05:00
Merge branch '2026.2-rc'
This commit is contained in:
commit
9f8f0a94dd
7
.github/workflows/test-trigger.yml
vendored
7
.github/workflows/test-trigger.yml
vendored
@ -6,11 +6,10 @@ name: Trigger E2E tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ staging, 2025.9-rc, 2025.9-test ]
|
||||
branches: [ staging, 2025.12-rc, 2025.12-test ]
|
||||
|
||||
jobs:
|
||||
trigger-tests:
|
||||
if: github.ref_name == 'staging'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger E2E test run (local)
|
||||
@ -19,9 +18,9 @@ jobs:
|
||||
|
||||
if [ "$branch" = "staging" ]; then
|
||||
event_type="trigger-e2e-tests-staging"
|
||||
elif [ "$branch" = "2025.9-rc" ]; then
|
||||
elif [ "$branch" = "2025.12-rc" ]; then
|
||||
event_type="trigger-e2e-tests-rc"
|
||||
elif [ "$branch" = "2025.9-test" ]; then
|
||||
elif [ "$branch" = "2025.12-test" ]; then
|
||||
event_type="trigger-e2e-tests-test"
|
||||
fi
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -101,6 +101,9 @@ tags
|
||||
# ---> VisualStudioCode
|
||||
.vscode/*
|
||||
|
||||
# ---> Intellij
|
||||
.idea/*
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
|
||||
100
CHANGELOG.md
100
CHANGELOG.md
@ -5,6 +5,106 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
|
||||
# ❄️ Winter release (2026.2.0)
|
||||
|
||||
## Goals
|
||||
|
||||
This release upgrades our office applications with OnlyOffice 9 as well as the same history browsing feature as our other applications (history was limited until now). We also include a redesigned folder tree in the drive, and many other improvements and fixes as detailed below.
|
||||
|
||||
## Features
|
||||
|
||||
- Upgrade Office applications to OnlyOffice v9.2.0.119
|
||||
- OnlyOffice history browsing [#2134](https://github.com/cryptpad/cryptpad/pull/2134)
|
||||
- Update drive tree UI [#2102](https://github.com/cryptpad/cryptpad/pull/2102)
|
||||
|
||||
## Improvements
|
||||
|
||||
- Integration API improvements [#2139](https://github.com/cryptpad/cryptpad/pull/2139)
|
||||
- Integration API: Allow client download [#2114](https://github.com/cryptpad/cryptpad/pull/2114)
|
||||
- Let the user close the old password error modal, without having to refresh the login page [#2125](https://github.com/cryptpad/cryptpad/pull/2125)
|
||||
- Add support for required poll questions in forms [#2092](https://github.com/cryptpad/cryptpad/pull/2092)
|
||||
- Improve support & moderation UI [#2082](https://github.com/cryptpad/cryptpad/pull/2082)
|
||||
- Enable Dutch language [#2148](https://github.com/cryptpad/cryptpad/pull/2148)
|
||||
|
||||
## Fixes
|
||||
|
||||
- Account, Profile and Contacts
|
||||
- Fix incorrect error message for invalid credentials [#2163](https://github.com/cryptpad/cryptpad/pull/2163)
|
||||
- Delete 2FA data when archiving account [#2177](https://github.com/cryptpad/cryptpad/pull/2177)
|
||||
- Fix padding for profile action buttons [#2081](https://github.com/cryptpad/cryptpad/pull/2081)
|
||||
- Fix contact page name overflow [#2084](https://github.com/cryptpad/cryptpad/pull/2084)
|
||||
- Fix: Profile description editor now shows saved value without requiring focus [#2100](https://github.com/cryptpad/cryptpad/pull/2100)
|
||||
- Drives
|
||||
- Fix rtChannel not always stored and pinned [#2168](https://github.com/cryptpad/cryptpad/pull/2168)
|
||||
- Preserve drive list sorting order across reloads [#2115](https://github.com/cryptpad/cryptpad/pull/2115)
|
||||
- Fix: Removed avatars are no longer shown in team roster [#2087](https://github.com/cryptpad/cryptpad/pull/2087)
|
||||
- Fix text overflow issues on Ctrl+E (New document) modal [#2117](https://github.com/cryptpad/cryptpad/pull/2117)
|
||||
- Fix: Context menu new document options now respect selected type [#2124](https://github.com/cryptpad/cryptpad/pull/2124)
|
||||
- Apps
|
||||
- Hide History button if history disabled [#2104](https://github.com/cryptpad/cryptpad/pull/2104)
|
||||
- Disable dragging when selecting text inside title and items for Kanban cards [#2088](https://github.com/cryptpad/cryptpad/pull/2088)
|
||||
- Add missing margin to elements in Form [#2083](https://github.com/cryptpad/cryptpad/pull/2083)
|
||||
- CKEditor undo stack cleared on refresh [#2121](https://github.com/cryptpad/cryptpad/pull/2121)
|
||||
- change code-pad to code [#2160](https://github.com/cryptpad/cryptpad/pull/2160) @achf01
|
||||
- Office apps
|
||||
- Do not delete last checkpoint on 5xx error [#2166](https://github.com/cryptpad/cryptpad/pull/2166)
|
||||
- OnlyOffice tab notifications [#2110](https://github.com/cryptpad/cryptpad/pull/2110)
|
||||
- install-onlyoffice.sh: fix --check for new install_version function [#2097](https://github.com/cryptpad/cryptpad/pull/2097) @martinetd
|
||||
- Typos
|
||||
- Fix some typos [#2126](https://github.com/cryptpad/cryptpad/pull/2126) @josep11
|
||||
- doc: typo [#2116](https://github.com/cryptpad/cryptpad/pull/2116) @thiswillbeyourgithub
|
||||
- Fix translation key errors in admin panel and installation page [#2138](https://github.com/cryptpad/cryptpad/pull/2138)
|
||||
- Other
|
||||
- File upload base64 caused by base64 conversion [#2181](https://github.com/cryptpad/cryptpad/pull/2181)
|
||||
- Calendar export fixes [#2151](https://github.com/cryptpad/cryptpad/pull/2151)
|
||||
- Fix UI overflow when loading a PDF file [#2118](https://github.com/cryptpad/cryptpad/pull/2118)
|
||||
- Icon fixes [#2096](https://github.com/cryptpad/cryptpad/pull/2096)
|
||||
- moderator setting on by default [#2144](https://github.com/cryptpad/cryptpad/pull/2144)
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Minor upgrades
|
||||
- body-parser from ^1.20.2 to ^1.20.4
|
||||
- express from ~4.21.2 to ~4.22.1
|
||||
- jsonwebtoken from ^9.0.0 to ^9.0.3
|
||||
|
||||
|
||||
## Deployment and Development
|
||||
|
||||
- Do not deploy Draw.io's JAR files [#2120](https://github.com/cryptpad/cryptpad/pull/2120)
|
||||
- Switch to docker alpine image [#2119](https://github.com/cryptpad/cryptpad/pull/2119)
|
||||
- Update triggers for end-to-end test suite [#2060](https://github.com/cryptpad/cryptpad/pull/2060)
|
||||
- add Intellij directory to ignored git files [#2122](https://github.com/cryptpad/cryptpad/pull/2122) @josep11
|
||||
|
||||
|
||||
## Upgrade notes
|
||||
|
||||
If you are upgrading from a version older than `2025.9.0` please read the upgrade notes of all versions between yours and `2026.2.0` to avoid configuration issues.
|
||||
|
||||
To upgrade:
|
||||
|
||||
1. Stop your server
|
||||
2. Get the latest code with git
|
||||
|
||||
```bash
|
||||
git fetch --depth 1 origin tag 2026.2.0
|
||||
git checkout 2026.2.0
|
||||
npm ci
|
||||
npm run install:components
|
||||
./install-onlyoffice.sh
|
||||
```
|
||||
|
||||
3. Restart your server
|
||||
4. Review your instance's checkup page to ensure that you are passing all tests
|
||||
|
||||
## Contributors
|
||||
|
||||
Community: @achf01 @martinetd @josep11 @thiswillbeyourgithub
|
||||
|
||||
CryptPad team: @AAAMON @Chouhartem @dariiing @davidbenque @DianaXWiki @mathilde-cryptpad @wginolas @yflory @zuzanna-maria
|
||||
|
||||
|
||||
# 🍁 Autumn release (2025.9.0)
|
||||
|
||||
## Goals
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
# Multistage build to reduce image size and increase security
|
||||
FROM node:lts-slim AS build
|
||||
FROM node:lts-alpine AS build
|
||||
|
||||
# Create folder for CryptPad
|
||||
RUN mkdir /cryptpad
|
||||
@ -20,19 +20,14 @@ RUN npm install --production \
|
||||
&& npm run install:components
|
||||
|
||||
# Create actual CryptPad image
|
||||
FROM node:lts-slim
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
FROM node:lts-alpine
|
||||
|
||||
# Create user and group for CryptPad so it does not run as root
|
||||
RUN groupadd cryptpad -g 4001
|
||||
RUN useradd cryptpad -u 4001 -g 4001 -d /cryptpad
|
||||
RUN addgroup -S cryptpad -g 4001 && adduser -S cryptpad -G cryptpad --uid 4001 -h /cryptpad
|
||||
|
||||
# Install curl for healthcheck
|
||||
# Install git, rdfind and unzip for install-onlyoffice.sh
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
curl ca-certificates git rdfind unzip && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache ca-certificates git rdfind unzip bash curl
|
||||
|
||||
# Copy cryptpad with installed modules
|
||||
COPY --from=build --chown=cryptpad /cryptpad /cryptpad
|
||||
|
||||
@ -50,7 +50,7 @@ module.exports = {
|
||||
*/
|
||||
httpUnsafeOrigin: 'http://localhost:3000',
|
||||
|
||||
/* httpSafeOrigin is the URL that is used for the 'sandbox' described above.
|
||||
/* httpSafeOrigin is the URL used for the 'sandbox' described above.
|
||||
* If you're testing or developing with CryptPad on your local machine then
|
||||
* it is appropriate to leave this blank. The default behaviour is to serve
|
||||
* the main domain over port 3000 and to serve the sandbox content over port 3001.
|
||||
@ -256,8 +256,8 @@ module.exports = {
|
||||
*/
|
||||
archivePath: './data/archive',
|
||||
|
||||
/* CryptPad allows logged in users to request that particular documents be
|
||||
* stored by the server indefinitely. This is called 'pinning'.
|
||||
/* CryptPad allows logged in users to request that the server
|
||||
* store particular documents indefinitely. This is called 'pinning'.
|
||||
* Pin requests are stored in a pin-store. The location of this store is
|
||||
* defined here.
|
||||
*/
|
||||
|
||||
@ -151,10 +151,20 @@ define([
|
||||
break;
|
||||
*/
|
||||
case 'DELETED_USER':
|
||||
UI.errorLoadingScreen(
|
||||
UI.getDestroyedPlaceholder(result.reason, true), true, () => {
|
||||
window.location.reload();
|
||||
if (result.reason === 'PASSWORD_CHANGE') {
|
||||
UI.removeLoadingScreen(function () {
|
||||
UI.alert(Messages.dph_account_pw, function () {
|
||||
hashing = false;
|
||||
$('#password').focus();
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
UI.errorLoadingScreen(
|
||||
UI.getDestroyedPlaceholder(result.reason, true), true, () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'INVAL_PASS':
|
||||
UI.removeLoadingScreen(function () {
|
||||
|
||||
@ -20,7 +20,7 @@ var map = {
|
||||
'it': 'Italiano',
|
||||
'ja': '日本語',
|
||||
'nb': 'Norwegian Bokmål',
|
||||
//'nl': 'Nederlands'
|
||||
'nl': 'Nederlands',
|
||||
'pl': 'Polski',
|
||||
'pt-br': 'Português do Brasil',
|
||||
'pt-pt': 'Português do Portugal',
|
||||
|
||||
@ -41,7 +41,7 @@ define([
|
||||
h('ul.cp-notes-list', [
|
||||
h('li', [
|
||||
Icons.get('alert'),
|
||||
Msg.admin_note
|
||||
Msg.admin_creation_note
|
||||
]),
|
||||
h('li', [
|
||||
Icons.get('alert'),
|
||||
@ -92,4 +92,3 @@ define([
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -82,10 +82,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cp-admin-message {
|
||||
color: @cryptpad_text_col;
|
||||
}
|
||||
|
||||
.cp-inline-alert-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@ -10,6 +10,6 @@
|
||||
@browser_media-not-big: 800px;
|
||||
@browser_media-not-small: 801px;
|
||||
|
||||
@browser_media-short-screen: 450px;
|
||||
@browser_media-short-screen: 500px;
|
||||
@browser_media-narrow-screen: 400px;
|
||||
@browser_media-medium-screen: 600px;
|
||||
|
||||
@ -101,7 +101,6 @@
|
||||
margin: 8px 0;
|
||||
}
|
||||
svg {
|
||||
margin: auto;
|
||||
margin: 18px 0;
|
||||
text-align: center;
|
||||
&.listonly {
|
||||
@ -316,7 +315,7 @@
|
||||
overflow: auto;
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
max-width: 500px;
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
padding: 10px 0px 0px 0px;
|
||||
color: @cp_sidebar-left-fg;
|
||||
@ -342,13 +341,17 @@
|
||||
.cp-app-drive-tree-docs {
|
||||
.droppable-tree-element();
|
||||
margin-top: 15px;
|
||||
//padding: 0 0 0 20px;
|
||||
padding: 0;
|
||||
padding: 5px;
|
||||
background-color: @cp_sidebar-left-item-bg;
|
||||
border-radius: @variables_radius;
|
||||
border-radius: @variables_radius_L;
|
||||
cursor: auto;
|
||||
&li, li {
|
||||
padding: 0;
|
||||
padding: 0px;
|
||||
ul {
|
||||
li {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
&.cp-app-drive-element-collapsed ul {
|
||||
display: none;
|
||||
}
|
||||
@ -363,17 +366,18 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.leftside-menu-category_main();
|
||||
border-radius: @variables_radius_M;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin: 0 0 2px -5px;
|
||||
margin: 0;
|
||||
.cp-app-drive-element {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
&:focus-visible {
|
||||
&:focus-visible, input:focus-visible {
|
||||
outline: @variables_focus_style;
|
||||
}
|
||||
}
|
||||
@ -386,7 +390,7 @@
|
||||
margin: 0;
|
||||
margin-top: 10px;
|
||||
background-color: @cp_sidebar-left-item-bg;
|
||||
border-radius: @variables_radius;
|
||||
border-radius: @variables_radius_L;
|
||||
box-shadow: @cryptpad_ui_shadow;
|
||||
.cp-app-drive-tree-root {
|
||||
.droppable-tree-element();
|
||||
@ -415,29 +419,19 @@
|
||||
.cp-app-drive-icon-expcol {
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: -17px;
|
||||
top: 10px;
|
||||
left: -20px;
|
||||
overflow: hidden;
|
||||
width: calc(1em - 2px) !important;
|
||||
height: calc(1em - 2px) !important;
|
||||
background-color: @cp_sidebar-left-item-bg;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 10;
|
||||
cursor: default;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
svg {
|
||||
margin: -1px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin: 0;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
svg {
|
||||
outline: @variables_focus_style;
|
||||
border-radius: @variables_radius_S;
|
||||
margin: 0;
|
||||
}
|
||||
outline: @variables_focus_style;
|
||||
border-radius: @variables_radius;
|
||||
}
|
||||
}
|
||||
.cp-app-drive-tree-docs {
|
||||
@ -451,9 +445,8 @@
|
||||
}
|
||||
}
|
||||
.cp-app-drive-tree-root > .cp-app-drive-element-row {
|
||||
margin-left: 0px;
|
||||
.cp-icon-hidden ~ svg {
|
||||
margin-left: -21px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
.cp-app-drive-icon-expcol {
|
||||
top: -1px;
|
||||
@ -478,38 +471,17 @@
|
||||
padding-left: 15px;
|
||||
}
|
||||
&> ul {
|
||||
padding-left: 30px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand/collapse lines
|
||||
.cp-app-drive-tree-docs ul {
|
||||
margin: 0px 0px 0px 10px;
|
||||
margin: 0px 0px 0px 12px;
|
||||
list-style: none;
|
||||
padding-left: 10px;
|
||||
li {
|
||||
position: relative;
|
||||
&:before {
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: -10px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-left: 1px solid @cp_drive-tree-branch;
|
||||
height: ~"calc(1em + 11px)";
|
||||
border-bottom: 1px solid @cp_drive-tree-branch;
|
||||
width: 11px;
|
||||
}
|
||||
&:after {
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
bottom: -7px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-left: 1px solid @cp_drive-tree-branch;
|
||||
height: 100%;
|
||||
}
|
||||
&.cp-app-drive-tree-root {
|
||||
margin: 0px 0px 0px -10px;
|
||||
&:before {
|
||||
@ -551,6 +523,7 @@
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
border-left: 1px solid transparent;
|
||||
&:hover {
|
||||
border-left: 1px solid @cryptpad_text_col;
|
||||
}
|
||||
@ -1062,7 +1035,10 @@
|
||||
|
||||
.cp-app-drive-new-name {
|
||||
color: @cp_drive-fg;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: @browser_media-medium-screen),
|
||||
|
||||
@ -10,12 +10,12 @@
|
||||
|
||||
.leftside-menu-category_main() {
|
||||
.unselectable_make();
|
||||
padding: 5px;
|
||||
padding: 5px 5px 5px 10px;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
height: @variables_bar-height;
|
||||
border-radius: @variables_radius;
|
||||
background-color: @cp_sidebar-left-item-bg;
|
||||
border-radius: @variables_radius_L;
|
||||
&:hover {
|
||||
background: @cryptpad_text_col_inv;
|
||||
}
|
||||
|
||||
@ -109,6 +109,8 @@
|
||||
min-width: 0;
|
||||
.cp-app-contacts-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.cp-app-contacts-icons {
|
||||
text-align: right;
|
||||
|
||||
@ -365,6 +365,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.alertify .dialog .msg div.cp-admin-message {
|
||||
color: @cryptpad_text_col;
|
||||
a {
|
||||
color: @cryptpad_color_link;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-userteam-picker {
|
||||
.cp-userteam-filter {
|
||||
margin: 5px 0;
|
||||
|
||||
@ -68,6 +68,9 @@
|
||||
&.cp-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
a {
|
||||
color: @cryptpad_color_link;
|
||||
}
|
||||
}
|
||||
.cp-notification-dismiss {
|
||||
color: @cp_dropdown-fg;
|
||||
|
||||
@ -121,6 +121,11 @@
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: baseline;
|
||||
input, textarea {
|
||||
&:focus-visible {
|
||||
outline: @variables_focus_style;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-usergrid-container {
|
||||
margin-bottom: 0px !important;
|
||||
@ -219,6 +224,9 @@
|
||||
border-right: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
&:focus-visible {
|
||||
outline: @variables_focus_style;
|
||||
}
|
||||
}
|
||||
input:invalid {
|
||||
border-color: red;
|
||||
|
||||
@ -40,6 +40,14 @@
|
||||
padding: 10px 15px;
|
||||
height: 300px;
|
||||
}
|
||||
textarea, input {
|
||||
&:focus-visible {
|
||||
outline: @variables_focus_style;
|
||||
}
|
||||
}
|
||||
.cp-support-list-send, .cp-support-reply-cancel {
|
||||
margin-right: @variables_icon_margin;
|
||||
}
|
||||
}
|
||||
.cp-support-attachments {
|
||||
display: flex;
|
||||
@ -58,6 +66,9 @@
|
||||
.cp-support-recorded-insert {
|
||||
margin-right: @sidebar_base-margin;
|
||||
}
|
||||
button {
|
||||
margin-right: @variables_icon_margin;
|
||||
}
|
||||
}
|
||||
@ticket-padding: 10px;
|
||||
.cp-support-container {
|
||||
@ -72,7 +83,7 @@
|
||||
border-radius: @variables_radius;
|
||||
.cp-support-ticket-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
.cp-support-header-data {
|
||||
display: flex;
|
||||
@ -82,7 +93,7 @@
|
||||
min-width: 0;
|
||||
.cp-support-ticket-title {
|
||||
width: 100%;
|
||||
margin-bottom: @sidebar_base-margin;
|
||||
margin-bottom: 1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
.cp-tags-list {
|
||||
@ -91,10 +102,15 @@
|
||||
}
|
||||
margin-top: @sidebar_base-margin;
|
||||
width: 100%;
|
||||
span {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-support-title-buttons {
|
||||
margin-left: 1em;
|
||||
display: inline-flex;
|
||||
gap: @variables_icon_margin;
|
||||
svg {
|
||||
margin: 0;
|
||||
}
|
||||
@ -103,6 +119,7 @@
|
||||
.cp-support-ticket-tags {
|
||||
.cp-tokenfield-form button.cp-token-close {
|
||||
border-radius: @variables_radius !important;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
&.cp-filtered {
|
||||
@ -186,6 +203,7 @@
|
||||
}
|
||||
.cp-support-list-actions {
|
||||
display: flex;
|
||||
gap: @variables_icon_margin;
|
||||
order: 3;
|
||||
margin-top: 5px;
|
||||
.cp-support-hide {
|
||||
|
||||
@ -43,6 +43,9 @@
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 0 !important;
|
||||
&:focus-visible {
|
||||
outline: @variables_focus_style;
|
||||
}
|
||||
}
|
||||
button {
|
||||
// no radius in input + button combo
|
||||
@ -174,9 +177,6 @@
|
||||
margin-right: @tokens_margin;
|
||||
}
|
||||
.tools_unselectable();
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.cp-tags-list {
|
||||
em {
|
||||
@ -186,6 +186,7 @@
|
||||
span {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-bottom: 0.3rem;
|
||||
.tools_unselectable();
|
||||
&.active {
|
||||
background-color: @cp_kanban-fg;
|
||||
|
||||
@ -56,6 +56,12 @@
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.cp-toolbar-history-previous, .cp-toolbar-history-next {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.cp-toolbar-history-actions {
|
||||
display: flex;
|
||||
@ -129,9 +135,7 @@
|
||||
}
|
||||
&.cp-smallpatch {
|
||||
.cp-history-snapshot {
|
||||
border: none !important;
|
||||
width: 2px !important;
|
||||
background: @pos-color;
|
||||
border: 2px solid !important;
|
||||
}
|
||||
.cp-history-timeline-pos {
|
||||
border-right: none;
|
||||
@ -145,6 +149,7 @@
|
||||
height: 20px !important;
|
||||
.cp-history-timeline-pos {
|
||||
height: 20px !important;
|
||||
margin-left: 100%;
|
||||
}
|
||||
}
|
||||
.cp-history-timeline-actions {
|
||||
@ -179,10 +184,25 @@
|
||||
background-color: @history_lineBg;
|
||||
height: 39px;
|
||||
}
|
||||
.cp-oohistory-bar-el {
|
||||
background-color: @history_userBg2;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.cp-history-patch {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
&:nth-child(odd) {
|
||||
background-color: @history_userBg1
|
||||
}
|
||||
}
|
||||
|
||||
.cp-history-timeline-bar {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding: 1px;
|
||||
& > span {
|
||||
height: 18px;
|
||||
display: flex;
|
||||
@ -207,19 +227,29 @@
|
||||
.cp-history-snapshots {
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
bottom: 1px;
|
||||
cursor: pointer;
|
||||
.cp-history-snapshot {
|
||||
position: absolute;
|
||||
border: 2px solid @cryptpad_text_col;
|
||||
height: 100%;
|
||||
height: 98%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: center !important;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-history-snapshots-oo {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
margin-left: -1px;
|
||||
width: 100%;
|
||||
.cp-history-snapshot {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-history-timeline-actions {
|
||||
@ -260,6 +290,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cp-history-oo-timeline-pos {
|
||||
border: 2px solid @cryptpad_text_col;
|
||||
height: 37px;
|
||||
svg {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 70%;
|
||||
transform: translateX(-50%)
|
||||
}
|
||||
}
|
||||
|
||||
.cp-history-timeline-pos {
|
||||
//width: 2px;
|
||||
border: 2px solid @cryptpad_text_col;
|
||||
|
||||
@ -514,7 +514,7 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: @browser_media-medium-screen),
|
||||
screen and (max-height: 500px) {
|
||||
screen and (max-height: @browser_media-short-screen) {
|
||||
flex-wrap: wrap;
|
||||
height: @toolbar_line-height;
|
||||
.cp-toolbar-top-filler {
|
||||
@ -873,8 +873,8 @@
|
||||
// width: 100%;
|
||||
// }
|
||||
}
|
||||
@media screen and (max-height: @browser_media-medium-screen),
|
||||
screen and (max-width: @browser_media-medium-screen) {
|
||||
@media screen and (max-width: @browser_media-medium-screen),
|
||||
screen and (max-height: @browser_media-short-screen) {
|
||||
a.cp-toolbar-link-logo {
|
||||
svg {
|
||||
width: @variables_icon_width;
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
// Rounded corners
|
||||
@variables_radius_S: 3px;
|
||||
@variables_radius: 5px;
|
||||
@variables_radius_M: 7.5px;
|
||||
@variables_radius_L: 10px;
|
||||
|
||||
@variables_focus_style: @cryptpad_color_brand solid 2px;
|
||||
|
||||
@ -6,12 +6,18 @@ define([
|
||||
'jquery',
|
||||
'/common/hyperscript.js',
|
||||
'/customize/pages.js',
|
||||
'/customize/application_config.js',
|
||||
'/components/nthen/index.js',
|
||||
], function ($, h, Pages, nThen) {
|
||||
], function ($, h, Pages, AppConfig, nThen) {
|
||||
// we consider that there is no valid reason to load any of the info pages
|
||||
// in an iframe. abort everything if you detect that you are embedded.
|
||||
if (window.top !== window) { return; }
|
||||
|
||||
if (AppConfig.integrationOnly) {
|
||||
window.location.href = '/customize/404.html';
|
||||
return;
|
||||
}
|
||||
|
||||
$(function () {
|
||||
var $body = $('body');
|
||||
var pathname = location.pathname;
|
||||
|
||||
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
So you want to write a realtime collaborative application?
|
||||
|
||||
This guide will focus on applications which require **multiple clients** to **collaboratively construct a single authoratative document**.
|
||||
This guide will focus on applications which require **multiple clients** to **collaboratively construct a single authoritative document**.
|
||||
|
||||
[XWiki-Labs](https://labs.xwiki.com/) has published an open source suite (called [CryptPad](https://github.com/cryptpad/cryptpad)) of collaborative editors which employ end to end encryption.
|
||||
This guide will refer to the techniques used in the prototypes developed therein.
|
||||
@ -82,7 +82,7 @@ Chainpad can handle out of order messages, but it performs best when its message
|
||||
By architecting your system such that all clients send to a server which then relays to other clients, you guarantee that a particular chain of patches is consistent between the participants of your session.
|
||||
|
||||
CryptPad is capable of using a variety of data stores.
|
||||
Which data store your instance employs can be [easily configured](https://github.com/cryptpad/cryptpad/blob/master/config.example.js).
|
||||
Which data store your instance uses can be [easily configured](https://github.com/cryptpad/cryptpad/blob/master/config.example.js).
|
||||
|
||||
You simply need to write an adaptor which conforms to a simple API.
|
||||
The documentation for writing such an adaptor, and the complete list of implemented adaptors, is available [here](https://github.com/cryptpad/cryptpad/tree/master/storage).
|
||||
@ -133,7 +133,7 @@ It is the application developers' responsibility to infer from the new content w
|
||||
In the process of developing its [Realtime CKEditor](https://cryptpad.fr) XWiki Labs discovered that many of the [existing Javascript libraries](https://github.com/Matt-Esch/virtual-dom) for updating a **DOM** are very destructive, and replace elements completely once a difference is detected within the tree (when scanning left to right).
|
||||
|
||||
After testing several options, we ended up using a patched version of [**DiffDOM**](https://github.com/fiduswriter/diffDOM).
|
||||
Its diffing algorithm is _mostly correct_, but it lacks a move operation, namely, it determine that when the text of a paragraph gets bolded, that the textNode within the P should be moved into a STRONG element, with that STRONG element placed within the P.
|
||||
Its diffing algorithm is _mostly correct_, but it lacks a move operation, namely, it determines that when the text of a paragraph gets bolded, that the textNode within the P should be moved into a STRONG element, with that STRONG element placed within the P.
|
||||
As such, there are difficulties with applying styles to large portions of the document while someone is editing within that section.
|
||||
|
||||
On the whole, however, contentEditable documents avoid many of the problems that a text editor faces with its rendering cycle.
|
||||
|
||||
@ -27,17 +27,17 @@ main() {
|
||||
|
||||
ask_for_license
|
||||
|
||||
# Check if 'oldest_needed_version' is already set, if not, set it to v8
|
||||
# Check if 'oldest_needed_version' is already set, if not, set it to v9
|
||||
if [ -z "${PROPS['oldest_needed_version']+set}" ]; then
|
||||
echo "'oldest_needed_version' is not set. Setting it to v8."
|
||||
set_prop "oldest_needed_version" "v8"
|
||||
echo "'oldest_needed_version' is not set. Setting it to v9."
|
||||
set_prop "oldest_needed_version" "v9"
|
||||
else
|
||||
echo "'oldest_needed_version' is already set to ${PROPS['oldest_needed_version']}. No changes made."
|
||||
fi
|
||||
|
||||
mkdir -p "$OO_DIR"
|
||||
|
||||
available_versions=(v1 v2b v4 v5 v6 v7 v8 x2t)
|
||||
available_versions=(v1 v2b v4 v5 v6 v7 v8 v9 x2t)
|
||||
|
||||
start_installing=false
|
||||
for version in "${available_versions[@]}"; do
|
||||
@ -92,7 +92,13 @@ main() {
|
||||
# From all the older versions only v7 has 'dictionaries', we remove it for the same reasons
|
||||
rm -rf "$OO_DIR/v7/dictionaries/"
|
||||
;;
|
||||
v8) install_version v8 v8.3.3.23+5 de5056c12e1d91b054251b50a2326d5705ccd0c529b20b936ea1dc08d9d9458e99206de1fc52076af85df9352f1961c8ba9474c864f76bea59bce19b0595dd72 ;;
|
||||
v8) install_version v8 v8.3.3.23+5 de5056c12e1d91b054251b50a2326d5705ccd0c529b20b936ea1dc08d9d9458e99206de1fc52076af85df9352f1961c8ba9474c864f76bea59bce19b0595dd72
|
||||
rm -rf "$OO_DIR/v8/web-apps/apps/documenteditor/main/resources/help"
|
||||
rm -rf "$OO_DIR/v8/web-apps/apps/presentationeditor/main/resources/help"
|
||||
rm -rf "$OO_DIR/v8/web-apps/apps/spreadsheeteditor/main/resources/help"
|
||||
rm -rf "$OO_DIR/v8/web-apps/apps/common/main/resources/help/"
|
||||
;;
|
||||
v9) install_version v9 v9.2.0.119+3 e18b76c2f2e3021840e716b59049752d98b30790926af187302533d9851fe5be0ff3be402751dd33664241409f4638824f5c7dbab63b37695099194765b0542a ;;
|
||||
x2t) install_x2t v7.3+1 ab0c05b0e4c81071acea83f0c6a8e75f5870c360ec4abc4af09105dd9b52264af9711ec0b7020e87095193ac9b6e20305e446f2321a541f743626a598e5318c1 ;;
|
||||
*)
|
||||
echo "Unknown version: $version"
|
||||
@ -271,17 +277,27 @@ install_old_version() {
|
||||
}
|
||||
|
||||
install_version() {
|
||||
ensure_command_available curl
|
||||
ensure_command_available sha512sum
|
||||
ensure_command_available unzip
|
||||
|
||||
local DIR=$1
|
||||
local VERSION=$2
|
||||
local HASH=$3
|
||||
local FULL_DIR=$OO_DIR/$DIR
|
||||
local LAST_DIR=$(pwd)
|
||||
|
||||
if [ ! -e "$FULL_DIR"/.version ] || [ "$(cat "$FULL_DIR"/.version)" != "$VERSION" ]; then
|
||||
local ACTUAL_VERSION="not installed"
|
||||
if [ -e "$FULL_DIR"/.version ]; then
|
||||
ACTUAL_VERSION="$(cat "$FULL_DIR"/.version)"
|
||||
fi
|
||||
|
||||
if [ "$ACTUAL_VERSION" != "$VERSION" ]; then
|
||||
if [ ${CHECK+x} ]; then
|
||||
echo "Wrong version in $FULL_DIR. Expected: $VERSION. Found: $ACTUAL_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_command_available sha512sum
|
||||
ensure_command_available curl
|
||||
ensure_command_available unzip
|
||||
|
||||
rm -rf "$FULL_DIR"
|
||||
mkdir -p "$FULL_DIR"
|
||||
|
||||
@ -289,7 +305,7 @@ install_version() {
|
||||
|
||||
curl "https://github.com/cryptpad/onlyoffice-editor/releases/download/$VERSION/onlyoffice-editor.zip" --location --output "onlyoffice-editor.zip"
|
||||
echo "$HASH onlyoffice-editor.zip" >onlyoffice-editor.zip.sha512
|
||||
if ! sha512sum --check onlyoffice-editor.zip.sha512; then
|
||||
if ! sha512sum -c onlyoffice-editor.zip.sha512; then
|
||||
echo "onlyoffice-editor.zip does not match expected checksum"
|
||||
exit 1
|
||||
fi
|
||||
@ -332,7 +348,7 @@ install_x2t() {
|
||||
ensure_command_available unzip
|
||||
curl "https://github.com/cryptpad/onlyoffice-x2t-wasm/releases/download/$VERSION/x2t.zip" --location --output x2t.zip
|
||||
echo "$HASH x2t.zip" >x2t.zip.sha512
|
||||
if ! sha512sum --check x2t.zip.sha512; then
|
||||
if ! sha512sum -c x2t.zip.sha512; then
|
||||
echo "x2t.zip does not match expected checksum"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
28
lib/api.js
28
lib/api.js
@ -173,16 +173,16 @@ nThen(function (w) {
|
||||
let oldUsers = Env.maxConcurrentRegUsers || 0;
|
||||
let stats = Server.getSessionStats();
|
||||
let chans = Server.getActiveChannelCount();
|
||||
let reg = 0;
|
||||
let regKeys = [];
|
||||
Object.keys(Env.netfluxUsers).forEach(id => {
|
||||
let keys = Env.netfluxUsers[id];
|
||||
let key = Object.keys(keys || {})[0];
|
||||
if (!key) { return; }
|
||||
if (regKeys.includes(key)) { return; }
|
||||
reg++;
|
||||
regKeys.push(key);
|
||||
});
|
||||
|
||||
const map = Env.netfluxUsers;
|
||||
// Extract public key from each ws connection
|
||||
let regKeys = Object.keys(map).map(id => {
|
||||
return Object.keys(map[id] || {})[0];
|
||||
}).filter(Boolean);
|
||||
// Convert to set to get only unique values
|
||||
let regSet = new Set(regKeys);
|
||||
const reg = regSet.size;
|
||||
|
||||
Env.maxConcurrentWs = Math.max(oldWs, stats.total);
|
||||
Env.maxConcurrentUniqueWs = Math.max(oldUniqueWs, stats.unique);
|
||||
Env.maxConcurrentRegUsers = Math.max(oldUsers, reg);
|
||||
@ -194,9 +194,10 @@ nThen(function (w) {
|
||||
try {
|
||||
let users = Env.netfluxUsers || {};
|
||||
let online = Server.getOnlineUsers() || [];
|
||||
let onlineSet = new Set(online);
|
||||
let removed = 0;
|
||||
Object.keys(users).forEach(id => {
|
||||
if (!online.includes(id)) {
|
||||
if (!onlineSet.has(id)) {
|
||||
delete users[id];
|
||||
removed++;
|
||||
}
|
||||
@ -209,15 +210,16 @@ nThen(function (w) {
|
||||
let HK = require('./hk-utils');
|
||||
let chans = Env.channel_cache || {};
|
||||
let active = Server.getActiveChannels() || [];
|
||||
let activeSet = new Set(active);
|
||||
let removed = 0;
|
||||
Object.keys(chans).forEach(id => {
|
||||
if (!active.includes(id)) {
|
||||
if (!activeSet.has(id)) {
|
||||
HK.dropChannel(Env, id);
|
||||
removed++;
|
||||
}
|
||||
});
|
||||
if (Env.store) {
|
||||
Env.store.closeInactiveChannels(active);
|
||||
Env.store.closeInactiveChannels(activeSet);
|
||||
}
|
||||
if (removed) {
|
||||
Env.Log.info("CLEANED_ACTIVE_CHANNELS_MAP", {removed});
|
||||
|
||||
@ -14,6 +14,7 @@ const Meta = require("./metadata");
|
||||
const Logger = require("./log");
|
||||
const plugins = require("./plugin-manager");
|
||||
const HK = require('./hk-util');
|
||||
const MFA = require("./storage/mfa");
|
||||
|
||||
let SSOUtils = plugins.SSO && plugins.SSO.utils;
|
||||
|
||||
@ -212,6 +213,7 @@ COMMANDS.start = (edPublic, blockId, reason) => {
|
||||
}
|
||||
Log.info('MODERATION_ACCOUNT_BLOCK', safeKey, waitFor());
|
||||
}));
|
||||
MFA.delete(Env, blockId, waitFor());
|
||||
if (!SSOUtils) { return; }
|
||||
SSOUtils.deleteAccount(Env, blockId, waitFor((err) => {
|
||||
if (err) {
|
||||
|
||||
@ -82,12 +82,7 @@ Pinning.getTotalSize = function (Env, safeKey, cb) {
|
||||
var batchKey = (limit && Array.isArray(limit.users)) ? limit.users.join('') : safeKey;
|
||||
|
||||
Env.batchTotalSize(batchKey, cb, function (done) {
|
||||
var channels = [];
|
||||
|
||||
var addUnique = function (channel) {
|
||||
if (channels.indexOf(channel) !== -1) { return; }
|
||||
channels.push(channel);
|
||||
};
|
||||
let channels = new Set();
|
||||
|
||||
nThen(function (waitFor) {
|
||||
// Get the channels list for our user account
|
||||
@ -96,7 +91,10 @@ Pinning.getTotalSize = function (Env, safeKey, cb) {
|
||||
waitFor.abort();
|
||||
return done('INVALID_PIN_LIST');
|
||||
}
|
||||
_channels.forEach(addUnique);
|
||||
|
||||
for (let channel of _channels) {
|
||||
channels.add(channel);
|
||||
}
|
||||
}));
|
||||
// Get the channels list for users sharing our quota
|
||||
if (limit && Array.isArray(limit.users) && limit.users.length > 1) {
|
||||
@ -104,12 +102,14 @@ Pinning.getTotalSize = function (Env, safeKey, cb) {
|
||||
if (key === unsafeKey) { return; } // Don't count ourselves twice
|
||||
getChannelList(Env, key, waitFor(function (_channels) {
|
||||
if (!_channels) { return; } // Broken user, don't count their quota
|
||||
_channels.forEach(addUnique);
|
||||
for (let channel of _channels) {
|
||||
channels.add(channel);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
}).nThen(function () {
|
||||
Env.getTotalSize(channels, done);
|
||||
Env.getTotalSize(Array.from(channels), done);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -444,6 +444,25 @@ app.use('/block/', function (req, res, next) {
|
||||
w.abort();
|
||||
next();
|
||||
}
|
||||
}).nThen(function (w) {
|
||||
// Block is protected with 2FA or SSO, make sure it still exists
|
||||
const url = req.url;
|
||||
if (typeof(url) !== "string") { return; }
|
||||
const s = url.split('/');
|
||||
const id = s[2];
|
||||
if (!(s[1]?.length === 2 && BlockStore.isValidKey(id))) { return; }
|
||||
BlockStore.isAvailable(Env, id, w((err, val) => {
|
||||
if (err) { return; }
|
||||
if (val !== false) { return; }
|
||||
// Block doesn't exist, send the placeholder
|
||||
w.abort();
|
||||
return BlockStore.readPlaceholder(Env, id, reason => {
|
||||
res.status(404).json({
|
||||
reason,
|
||||
code: 404
|
||||
});
|
||||
});
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
// We should only be able to reach this logic
|
||||
// if we successfully loaded and parsed some JSON
|
||||
|
||||
@ -286,9 +286,9 @@ var closeChannel = function (env, channelName, cb) {
|
||||
}
|
||||
};
|
||||
|
||||
var closeInactiveChannels = function (env, schedule, active) {
|
||||
var closeInactiveChannels = function (env, schedule, activeSet) {
|
||||
Object.keys(env.channels).forEach(channelName => {
|
||||
if (!active.includes(channelName)) {
|
||||
if (!activeSet.has(channelName)) {
|
||||
schedule.ordered(channelName, function (next) {
|
||||
closeChannel(env, channelName, next);
|
||||
});
|
||||
@ -1473,8 +1473,8 @@ module.exports.create = function (conf, _cb) {
|
||||
closeChannel(env, channelName, Util.both(cb, next));
|
||||
});
|
||||
},
|
||||
closeInactiveChannels: function (active) {
|
||||
closeInactiveChannels(env, schedule, active);
|
||||
closeInactiveChannels: function (activeSet) {
|
||||
closeInactiveChannels(env, schedule, activeSet);
|
||||
},
|
||||
// write to a log file
|
||||
log: function (channelName, content, cb) {
|
||||
|
||||
1831
package-lock.json
generated
1831
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cryptpad",
|
||||
"description": "a collaborative office suite that is end-to-end encrypted and open-source",
|
||||
"version": "2025.9.0",
|
||||
"version": "2026.2.0",
|
||||
"license": "AGPL-3.0+",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -15,7 +15,7 @@
|
||||
"@mcrowe/minibloom": "^0.2.0",
|
||||
"@node-saml/node-saml": "^5.1.0",
|
||||
"alertify.js": "1.0.11",
|
||||
"body-parser": "^1.20.2",
|
||||
"body-parser": "^1.20.4",
|
||||
"bootstrap": "^4.0.0",
|
||||
"bootstrap-tokenfield": "^0.12.0",
|
||||
"chainpad": "^5.2.6",
|
||||
@ -29,8 +29,8 @@
|
||||
"cookie-parser": "^1.4.7",
|
||||
"croppie": "^2.5.0",
|
||||
"dragula": "3.7.2",
|
||||
"drawio": "github:cryptpad/drawio-npm#npm-21.8.2+5",
|
||||
"express": "~4.21.2",
|
||||
"drawio": "github:cryptpad/drawio-npm#npm-21.8.2+6",
|
||||
"express": "~4.22.1",
|
||||
"file-saver": "1.3.1",
|
||||
"fs-extra": "^7.0.0",
|
||||
"get-folder-size": "^2.0.1",
|
||||
@ -39,7 +39,7 @@
|
||||
"hyper-json": "~1.4.0",
|
||||
"jquery": "3.6.0",
|
||||
"json.sortify": "github:cryptpad/JSON.sortify",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jszip": "3.10.1",
|
||||
"localforage": "^1.5.2",
|
||||
"marked": "^4.3.0",
|
||||
|
||||
@ -10,8 +10,16 @@ const factory = (NaclUtil) => {
|
||||
window.atob = window.atob || function (str) { return Buffer.from(str, 'base64').toString('binary'); };
|
||||
window.btoa = window.btoa || function (str) { return Buffer.from(str, 'binary').toString('base64'); };
|
||||
|
||||
Util.encodeBase64 = NaclUtil.encodeBase64;
|
||||
Util.encodeBase64 = u8 => {
|
||||
if (typeof (u8?.toBase64) === "function") {
|
||||
return u8.toBase64();
|
||||
}
|
||||
return NaclUtil.encodeBase64(u8);
|
||||
};
|
||||
Util.decodeBase64 = str => {
|
||||
if (typeof(Uint8Array?.fromBase64) === "function") {
|
||||
return Uint8Array.fromBase64(str);
|
||||
}
|
||||
let i = str.length % 4;
|
||||
if (i) { str += '='.repeat(4-i); }
|
||||
return NaclUtil.decodeBase64(str);
|
||||
@ -23,18 +31,6 @@ const factory = (NaclUtil) => {
|
||||
return Array.prototype.slice.call(A, start, end);
|
||||
};
|
||||
|
||||
Util.u8ToBase64 = (u8, cb) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
let res = reader.result;
|
||||
let trim = res.slice(res.indexOf(',') + 1);
|
||||
cb(trim);
|
||||
};
|
||||
reader.readAsDataURL(new Blob([u8]));
|
||||
};
|
||||
|
||||
|
||||
|
||||
Util.shuffleArray = function (a) {
|
||||
for (var i = a.length - 1; i > 0; i--) {
|
||||
var j = Math.floor(Math.random() * (i + 1));
|
||||
@ -314,13 +310,7 @@ const factory = (NaclUtil) => {
|
||||
};
|
||||
|
||||
Util.deduplicateString = function (array) {
|
||||
var a = array.slice();
|
||||
for(var i=0; i<a.length; i++) {
|
||||
for(var j=i+1; j<a.length; j++) {
|
||||
if(a[i] === a[j]) { a.splice(j--, 1); }
|
||||
}
|
||||
}
|
||||
return a;
|
||||
return Array.from(new Set(array));
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
(() => {
|
||||
const factory = () => {
|
||||
const version = 8;
|
||||
const version = 9;
|
||||
return {
|
||||
currentVersionNumber: version,
|
||||
currentVersion: 'v' + version
|
||||
|
||||
@ -430,7 +430,9 @@ const factory = (UserObject, Util, Hash,
|
||||
paths.forEach(function (path, idx) {
|
||||
var el = userObject.find(path);
|
||||
var files = [];
|
||||
var key = path[path.length - 1];
|
||||
// For trash paths, the name is at index 1, not the last element
|
||||
var isTrashPath = userObject.isPathIn && userObject.isPathIn(path, [userObject.TRASH]);
|
||||
var key = isTrashPath && path.length >= 2 ? path[1] : path[path.length - 1];
|
||||
|
||||
// Get the files ID from the current path (file or folder)
|
||||
if (userObject.isFile(el)) {
|
||||
@ -510,12 +512,17 @@ const factory = (UserObject, Util, Hash,
|
||||
|
||||
if (!newResolved.userObject.isFolder(newResolved.path)) { return void cb(); }
|
||||
|
||||
var moveError = null;
|
||||
nThen(function (waitFor) {
|
||||
if (resolved.main.length) {
|
||||
// Move from the main drive
|
||||
if (!newResolved.id) {
|
||||
// Move from the main drive to the main drive
|
||||
Env.user.userObject.move(resolved.main, newResolved.path, waitFor());
|
||||
Env.user.userObject.move(resolved.main, newResolved.path, waitFor(function (err) {
|
||||
if (err && !moveError) {
|
||||
moveError = err;
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// Move from the main drive to a shared folder
|
||||
|
||||
@ -523,12 +530,18 @@ const factory = (UserObject, Util, Hash,
|
||||
var toCopy = _getCopyFromPaths(Env, resolved.main, Env.user.userObject);
|
||||
var newUserObject = newResolved.userObject;
|
||||
toCopy.forEach(function (obj) {
|
||||
newUserObject.copyFromOtherDrive(newResolved.path, obj.el, obj.data, obj.key);
|
||||
try {
|
||||
newUserObject.copyFromOtherDrive(newResolved.path, obj.el, obj.data, obj.key);
|
||||
} catch (err) {
|
||||
if (!moveError) {
|
||||
moveError = err;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (copy) { return; }
|
||||
|
||||
if (resolved.main.length) {
|
||||
if (resolved.main.length && !moveError) {
|
||||
// Remove the elements from the old location (without unpinning)
|
||||
Env.user.userObject.delete(resolved.main, waitFor()); // FIXME waitFor() is called synchronously
|
||||
}
|
||||
@ -542,7 +555,11 @@ const factory = (UserObject, Util, Hash,
|
||||
var paths = resolved.folders[fId];
|
||||
if (newResolved.id === fId) {
|
||||
// Move to the same shared folder
|
||||
newResolved.userObject.move(paths, newResolved.path, waitFor());
|
||||
newResolved.userObject.move(paths, newResolved.path, waitFor(function (err) {
|
||||
if (err && !moveError) {
|
||||
moveError = err;
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// Move to a different shared folder or to main drive
|
||||
var uoFrom = Env.folders[fId].userObject;
|
||||
@ -551,17 +568,28 @@ const factory = (UserObject, Util, Hash,
|
||||
// Copy the elements to the new location
|
||||
var toCopy = _getCopyFromPaths(Env, paths, uoFrom);
|
||||
toCopy.forEach(function (obj) {
|
||||
uoTo.copyFromOtherDrive(newResolved.path, obj.el, obj.data, obj.key);
|
||||
try {
|
||||
uoTo.copyFromOtherDrive(newResolved.path, obj.el, obj.data, obj.key);
|
||||
} catch (err) {
|
||||
if (!moveError) {
|
||||
moveError = err;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (copy) { return; }
|
||||
|
||||
// Remove the elements from the old location (without unpinning)
|
||||
uoFrom.delete(paths, waitFor()); // FIXME waitFor() is called synchronously
|
||||
if (!moveError) {
|
||||
uoFrom.delete(paths, waitFor()); // FIXME waitFor() is called synchronously
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).nThen(function () {
|
||||
if (moveError) {
|
||||
return void cb(moveError);
|
||||
}
|
||||
cb();
|
||||
});
|
||||
};
|
||||
@ -1407,6 +1435,17 @@ const factory = (UserObject, Util, Hash,
|
||||
});
|
||||
};
|
||||
|
||||
const getMissingRtChannel = (Env) => {
|
||||
const userObjects = _getUserObjects(Env);
|
||||
const all = [];
|
||||
userObjects.forEach(function (uo) {
|
||||
const missing = uo.getMissingRtChannel();
|
||||
if (!missing) { return; }
|
||||
all.push(missing);
|
||||
});
|
||||
return all;
|
||||
};
|
||||
|
||||
var create = function (proxy, data, uoConfig) {
|
||||
var Env = {
|
||||
pinPads: data.pin,
|
||||
@ -1473,7 +1512,9 @@ const factory = (UserObject, Util, Hash,
|
||||
findFile: callWithEnv(findFile),
|
||||
getEditHash: callWithEnv(getEditHash),
|
||||
user: Env.user,
|
||||
folders: Env.folders
|
||||
folders: Env.folders,
|
||||
// Fix rtChannel
|
||||
getMissingRtChannel: callWithEnv(getMissingRtChannel)
|
||||
};
|
||||
};
|
||||
|
||||
@ -1490,7 +1531,12 @@ const factory = (UserObject, Util, Hash,
|
||||
path: path,
|
||||
newName: newName
|
||||
}
|
||||
}, cb);
|
||||
}, function (err, responseData) {
|
||||
if (err || responseData?.error) {
|
||||
return void cb(err || responseData);
|
||||
}
|
||||
cb();
|
||||
});
|
||||
};
|
||||
var moveInner = function (Env, paths, newPath, cb, copy) {
|
||||
return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", {
|
||||
@ -1500,7 +1546,12 @@ const factory = (UserObject, Util, Hash,
|
||||
newPath: newPath,
|
||||
copy: copy
|
||||
}
|
||||
}, cb);
|
||||
}, function (err, responseData) {
|
||||
if (err || responseData?.error) {
|
||||
return void cb(err || responseData);
|
||||
}
|
||||
cb();
|
||||
});
|
||||
};
|
||||
var emptyTrashInner = function (Env, deleteOwned, cb) {
|
||||
return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", {
|
||||
|
||||
@ -34,6 +34,8 @@ const factory = (Util, Hash, Realtime) => {
|
||||
|
||||
var debug = exp.debug;
|
||||
|
||||
let missingRtChannel = {};
|
||||
|
||||
exp._setReadOnly = function (state) {
|
||||
readOnly = state;
|
||||
if (!readOnly) { exp.fixFiles(); }
|
||||
@ -314,6 +316,14 @@ const factory = (Util, Hash, Realtime) => {
|
||||
// Copy file or folder
|
||||
var newParent = exp.find(path);
|
||||
var tempName = exp.isFile(element) ? Hash.createChannelId() : key;
|
||||
if (exp.isFolder(element) && tempName !== Messages.fm_newFolder &&
|
||||
typeof(newParent[tempName]) !== "undefined") {
|
||||
throw {
|
||||
error: 'E_DUPLICATE_FOLDER_NAME',
|
||||
folderName: tempName,
|
||||
message: Messages.fo_unavailableName
|
||||
};
|
||||
}
|
||||
var newName = exp.getAvailableName(newParent, tempName);
|
||||
if (Array.isArray(newParent)) {
|
||||
newParent.push(element);
|
||||
@ -375,8 +385,11 @@ const factory = (Util, Hash, Realtime) => {
|
||||
elementPath[1] : elementPath.pop();
|
||||
|
||||
if (typeof(newParent[newName]) !== "undefined") {
|
||||
exp.log(Messages.fo_unavailableName);
|
||||
return;
|
||||
return {
|
||||
error: 'E_DUPLICATE_FOLDER_NAME',
|
||||
folderName: newName,
|
||||
message: Messages.fo_unavailableName
|
||||
};
|
||||
}
|
||||
newParent[newName] = element;
|
||||
return true;
|
||||
@ -872,6 +885,10 @@ const factory = (Util, Hash, Realtime) => {
|
||||
// toClean.push(id);
|
||||
}
|
||||
|
||||
if (['sheet', 'doc', 'presentation'].includes(parsed.type) && !el.rtChannel) {
|
||||
missingRtChannel[el.channel] = el;
|
||||
}
|
||||
|
||||
if ((loggedIn || config.testMode) && rootFiles.indexOf(id) === -1) {
|
||||
debug("An element in filesData was not in ROOT, TEMPLATE or TRASH.", id, el);
|
||||
var newName = Hash.createChannelId();
|
||||
@ -980,6 +997,14 @@ const factory = (Util, Hash, Realtime) => {
|
||||
debug("File system was clean.", ms);
|
||||
};
|
||||
|
||||
exp.getMissingRtChannel = () => {
|
||||
const channels = missingRtChannel;
|
||||
missingRtChannel = {};
|
||||
if (readOnly) { return; }
|
||||
if (!Object.keys(channels).length) { return; }
|
||||
return channels;
|
||||
};
|
||||
|
||||
return exp;
|
||||
};
|
||||
|
||||
|
||||
@ -826,6 +826,7 @@ const factory = (Util, Hash,
|
||||
}
|
||||
// Copy the elements to their new location
|
||||
var toRemove = [];
|
||||
var duplicateError = null;
|
||||
paths.forEach(function (p) {
|
||||
var parentPath = p.slice();
|
||||
parentPath.pop();
|
||||
@ -835,11 +836,21 @@ const factory = (Util, Hash,
|
||||
return;
|
||||
}
|
||||
// Try to copy, and if success, remove the element from the old location
|
||||
if (exp.copyElement(p.slice(), newPath)) {
|
||||
var result = exp.copyElement(p.slice(), newPath);
|
||||
if (result === true) {
|
||||
toRemove.push(p);
|
||||
} else if (result && result.error === 'E_DUPLICATE_FOLDER_NAME') {
|
||||
if (!duplicateError) {
|
||||
duplicateError = result;
|
||||
}
|
||||
}
|
||||
});
|
||||
exp.delete(toRemove, cb);
|
||||
exp.delete(toRemove, function (err) {
|
||||
if (err || duplicateError) {
|
||||
return void cb(err || duplicateError);
|
||||
}
|
||||
if (typeof(cb) === "function") { cb(); }
|
||||
});
|
||||
};
|
||||
exp.restore = function (path, cb) {
|
||||
if (sframeChan) {
|
||||
@ -870,7 +881,16 @@ const factory = (Util, Hash,
|
||||
}, cb);
|
||||
}
|
||||
var parentEl = find(folderPath);
|
||||
var folderName = getAvailableName(parentEl, name || NEW_FOLDER_NAME);
|
||||
var requestedName = name || NEW_FOLDER_NAME;
|
||||
var defaultFolderName = NEW_FOLDER_NAME;
|
||||
if (requestedName !== defaultFolderName && typeof(parentEl[requestedName]) !== "undefined") {
|
||||
return void cb({
|
||||
error: 'E_DUPLICATE_FOLDER_NAME',
|
||||
folderName: requestedName,
|
||||
message: Messages.fo_unavailableName
|
||||
});
|
||||
}
|
||||
var folderName = getAvailableName(parentEl, requestedName);
|
||||
parentEl[folderName] = {};
|
||||
var newPath = folderPath.slice();
|
||||
newPath.push(folderName);
|
||||
@ -940,7 +960,13 @@ const factory = (Util, Hash,
|
||||
if (!newName || !newName.trim() || oldName === newName) { return; }
|
||||
var parentEl = find(parentPath);
|
||||
if (typeof(parentEl[newName]) !== "undefined") {
|
||||
log(Messages.fo_existingNameError);
|
||||
if (typeof cb === "function") {
|
||||
return void cb({
|
||||
error: 'E_DUPLICATE_FOLDER_NAME',
|
||||
folderName: newName,
|
||||
message: Messages.fo_unavailableName
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
parentEl[newName] = element;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
const factory = (Sortify, UserObject, ProxyManager,
|
||||
Migrate, Hash, Util, Constants, Feedback,
|
||||
Realtime, Messaging, Pinpad, Rpc, Cache,
|
||||
Realtime, Messaging, Pinpad, Rpc, Cryptget, Cache,
|
||||
SF, AccountTS, DriveTS, PadTS, Cursor,
|
||||
Support, Integration, OnlyOffice,
|
||||
Mailbox, Profile, Team, Messenger, History,
|
||||
@ -2231,6 +2231,72 @@ const factory = (Sortify, UserObject, ProxyManager,
|
||||
/////////////////////// Init /////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////
|
||||
|
||||
Store.fixMissingRtChannel = (cb) => {
|
||||
const all = {};
|
||||
const addMissing = (missing) => {
|
||||
missing.forEach(obj => {
|
||||
Object.keys(obj).forEach(chan => {
|
||||
let proxy = obj[chan];
|
||||
let href = proxy.roHref || proxy.href;
|
||||
all[chan] ||= {
|
||||
href,
|
||||
password: proxy.password,
|
||||
list: []
|
||||
};
|
||||
all[chan].list.push(proxy);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const mine = store.manager.getMissingRtChannel();
|
||||
addMissing(mine);
|
||||
} catch (e) {
|
||||
Feedback.send('MISSING_RT_CHANNEL_ERROR', true);
|
||||
return setTimeout(cb);
|
||||
}
|
||||
const teamsId = store.modules?.team?.getTeams() || [];
|
||||
teamsId.forEach(id => {
|
||||
const team = store.modules.team.getTeam(id);
|
||||
try {
|
||||
const teamMissing = team.manager.getMissingRtChannel();
|
||||
addMissing(teamMissing);
|
||||
} catch (e) {
|
||||
Feedback.send('MISSING_RT_CHANNEL_ERROR', true);
|
||||
return setTimeout(cb);
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(all).length) {
|
||||
Feedback.send('MISSING_RT_CHANNEL', true);
|
||||
}
|
||||
|
||||
let n = nThen;
|
||||
const opts = {
|
||||
network: store.network
|
||||
};
|
||||
Object.keys(all).forEach(chan => {
|
||||
n = n(waitFor => {
|
||||
const data = all[chan];
|
||||
const href = data.href;
|
||||
opts.password = data.password;
|
||||
const parsed = Hash.parsePadUrl(href);
|
||||
Cryptget.get(parsed.hash, waitFor((err, content) => {
|
||||
if (err) { return; }
|
||||
const p = Util.tryParse(content);
|
||||
const rtChannel = p?.content?.channel;
|
||||
if (!rtChannel) { return; }
|
||||
data.list.forEach(proxy => {
|
||||
proxy.rtChannel = rtChannel;
|
||||
});
|
||||
}), opts);
|
||||
}).nThen;
|
||||
});
|
||||
n(() => {
|
||||
setTimeout(cb);
|
||||
});
|
||||
};
|
||||
|
||||
Store.refreshDriveUI = function () {
|
||||
getAllStores().forEach(function (_s) {
|
||||
var send = _s.id ? _s.sendEvent : sendDriveEvent;
|
||||
@ -2384,6 +2450,8 @@ const factory = (Sortify, UserObject, ProxyManager,
|
||||
// Make teams non-blocking
|
||||
if (store.modules['team']) { store.modules['team'].onReady(waitFor); }
|
||||
});
|
||||
}).nThen(function (waitFor) {
|
||||
Store.fixMissingRtChannel(waitFor());
|
||||
}).nThen(function () {
|
||||
var requestLogin = function () {
|
||||
broadcast([], "REQUEST_LOGIN");
|
||||
@ -2721,7 +2789,7 @@ const factory = (Sortify, UserObject, ProxyManager,
|
||||
const start = (clientId, data, cb) => {
|
||||
// Don't create a drive if the user only wants to
|
||||
// open an existing pad
|
||||
const noDrive = data.neverDrive || (data.noDrive && !data.userHash && !data.anonHash);
|
||||
let noDrive = data.neverDrive || (data.noDrive && !data.userHash && !data.anonHash);
|
||||
if (noDrive) {
|
||||
if (data.neverDrive) { store.neverCache = true; }
|
||||
return void onNoDrive(clientId, obj => {
|
||||
@ -2983,6 +3051,7 @@ module.exports = factory(
|
||||
require('./components/messaging'),
|
||||
require('../common/pinpad'),
|
||||
require('../common/rpc'),
|
||||
require('../common/cryptget'),
|
||||
require('../common/cache-store'),
|
||||
require('./components/sharedfolder'),
|
||||
require('./components/account'), // .ts
|
||||
|
||||
@ -93,7 +93,7 @@ const _setMetadata: Callback = (ctx, clientId, data, cb) => {
|
||||
otherChannels.forEach(chan => {
|
||||
var _d = Util.clone(data);
|
||||
_d.channel = chan;
|
||||
Store.setPadMetadata(clientId, _d, () => {});
|
||||
s.rpc.setMetadata(_d, () => { });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -14,7 +14,7 @@ const factory = (Util, Hash,
|
||||
var TYPES = [
|
||||
'notifications',
|
||||
'supportteam',
|
||||
'broadcast'
|
||||
//'broadcast'
|
||||
];
|
||||
var BLOCKING_TYPES = [
|
||||
];
|
||||
|
||||
@ -119,6 +119,14 @@ const factory = (Util, Hash, Constants, Realtime,
|
||||
Messaging.updateMyData(ctx.store);
|
||||
ctx.updateMetadata();
|
||||
});
|
||||
} else if (key === 'avatar') {
|
||||
ctx.Store.set(null, {
|
||||
key: ['profile', 'avatar'],
|
||||
value: value || ""
|
||||
}, () => {
|
||||
Messaging.updateMyData(ctx.store);
|
||||
ctx.updateMetadata();
|
||||
});
|
||||
}
|
||||
cb(ctx.listmap.proxy);
|
||||
});
|
||||
|
||||
@ -568,7 +568,7 @@ define([
|
||||
row(Messages.admin_planName, data.plan || Messages.ui_none);
|
||||
|
||||
// plan note
|
||||
row(Messages.admin_creation_note, data.note || Messages.ui_none);
|
||||
row(Messages.admin_note, data.note || Messages.ui_none);
|
||||
|
||||
// storage limit
|
||||
if (data.limit) { row(Messages.admin_planlimit, getPrettySize(data.limit)); }
|
||||
@ -1415,6 +1415,7 @@ define([
|
||||
});
|
||||
|
||||
var getBlockId = (val) => {
|
||||
if (val.length === 44) { return val; }
|
||||
var url;
|
||||
try {
|
||||
url = new URL(val, ApiConfig.httpUnsafeOrigin);
|
||||
|
||||
@ -52,7 +52,7 @@ define([
|
||||
|
||||
|
||||
module.main = function (userDoc) {
|
||||
var content = userDoc.content;
|
||||
var content = userDoc.content || {};
|
||||
|
||||
var ICS = [
|
||||
'BEGIN:VCALENDAR',
|
||||
@ -227,7 +227,7 @@ define([
|
||||
|
||||
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
|
||||
r.until = d - 1; // Stop previous recursion
|
||||
delete r.count;
|
||||
addEvent(ICS, prev, null); // Add previous event
|
||||
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
|
||||
|
||||
@ -233,6 +233,11 @@ const factory = () => {
|
||||
// NOTE: this is only enforced client-side and will not prevent malicious clients from storing data
|
||||
AppConfig.disableAnonymousPadCreation = false;
|
||||
|
||||
// If your application is meant to be used exclusively with the
|
||||
// integration API, you can disable the direct access to the app by
|
||||
// setting integrationOnly to true
|
||||
AppConfig.integrationOnly = false;
|
||||
|
||||
// Hide the usage bar in settings and drive
|
||||
//AppConfig.hideUsageBar = true;
|
||||
|
||||
|
||||
@ -282,7 +282,7 @@ define([
|
||||
]);
|
||||
};
|
||||
|
||||
UI.tokenField = function (target, autocomplete) {
|
||||
UI.tokenField = function (target, autocomplete, close = null) {
|
||||
var t = {
|
||||
element: target || h('input'),
|
||||
};
|
||||
@ -350,6 +350,9 @@ define([
|
||||
});
|
||||
$form.append($input);
|
||||
$form.append($button);
|
||||
if (close) {
|
||||
$form.append(close);
|
||||
}
|
||||
if (isEdit) { $button.find('span').text(Messages.tag_edit); }
|
||||
else { $button.find('span').text(Messages.add); }
|
||||
$container.append($form);
|
||||
|
||||
@ -123,7 +123,12 @@ define([
|
||||
var legacyLogin = function (opt, isRegister, cb, res) {
|
||||
res = res || {};
|
||||
loadUserObject(opt, function (err, rt) {
|
||||
if (err) { return void cb(err); }
|
||||
if (err) {
|
||||
// If the channel is empty, it probably means the credentials
|
||||
// are invalid
|
||||
console.error(err);
|
||||
return void cb('NO_SUCH_USER');
|
||||
}
|
||||
|
||||
// if a proxy is marked as deprecated, it is because someone had a non-owned drive
|
||||
// but changed their password, and couldn't delete their old data.
|
||||
@ -316,6 +321,13 @@ define([
|
||||
res.opt = allocateBytes(bytes);
|
||||
res.blockHash = res.opt.blockHash;
|
||||
blockKeys = res.opt.blockKeys;
|
||||
if (window.location.hash === '#debug') {
|
||||
alert(JSON.stringify({
|
||||
blockId: Util.encodeBase64(blockKeys.sign.publicKey)
|
||||
}, 0, 2));
|
||||
waitFor.abort();
|
||||
return;
|
||||
}
|
||||
if (ssoAuth && ssoAuth.name) { uname = res.uname = ssoAuth.name; }
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
|
||||
@ -896,10 +896,6 @@ define([
|
||||
button = makeButton('print', 'cp-toolbar-icon-print', Messages.printButtonTitle2, Messages.printText);
|
||||
break;
|
||||
case 'history':
|
||||
if (!AppConfig.enableHistory) {
|
||||
button = $('<span>');
|
||||
break;
|
||||
}
|
||||
button = makeButton('history', 'cp-toolbar-icon-history', Messages.historyButton, Messages.historyText, Messages.historyButton);
|
||||
if (data.histConfig) {
|
||||
button.click(common.prepareFeedback(type)).on('click', function () {
|
||||
@ -907,6 +903,9 @@ define([
|
||||
UI.clearTooltipsDelay();
|
||||
});
|
||||
}
|
||||
if (!AppConfig.enableHistory) {
|
||||
button.css('display', 'none');
|
||||
}
|
||||
break;
|
||||
case 'mediatag':
|
||||
button = $(h('button.cp-toolbar-mediatag', {
|
||||
@ -1378,7 +1377,7 @@ define([
|
||||
|
||||
var apps = {
|
||||
pad: 'richtext',
|
||||
code: 'code-pad',
|
||||
code: 'code',
|
||||
slide: 'slides',
|
||||
sheet: 'sheets',
|
||||
poll: 'poll',
|
||||
@ -4523,6 +4522,259 @@ define([
|
||||
observer.start();
|
||||
};
|
||||
|
||||
var lexicographicCompare = function(a, b) {
|
||||
if (!Array.isArray(a)) {
|
||||
a = [a];
|
||||
}
|
||||
if (!Array.isArray(b)) {
|
||||
b = [b];
|
||||
}
|
||||
|
||||
if (a.length === 0 && b.length === 0) {
|
||||
return 0;
|
||||
} else if (a.length === 0) {
|
||||
return -1;
|
||||
} else if (b.length === 0) {
|
||||
return 1;
|
||||
} else if (typeof (a[0]) !== typeof (b[0])) {
|
||||
return String(a[0]) < String(b[0]) ? -1 : 1;
|
||||
} else {
|
||||
if (a[0] < b[0]) {
|
||||
return -1;
|
||||
} else if (a[0] > b[0]) {
|
||||
return 1;
|
||||
} else {
|
||||
return lexicographicCompare(a.slice(1), b.slice(1));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var splitStringToTextAndNumbers = function(s) {
|
||||
var textOrDigitsRe = /(?<text>\D+)?(?<digits>\d+)?/g;
|
||||
var split = [];
|
||||
|
||||
for (var match of s.matchAll(textOrDigitsRe)) {
|
||||
if (match.groups.text !== undefined) {
|
||||
split.push(match.groups.text);
|
||||
}
|
||||
if (match.groups.digits !== undefined) {
|
||||
split.push(parseInt(match.groups.digits));
|
||||
}
|
||||
}
|
||||
|
||||
return split;
|
||||
};
|
||||
|
||||
var naturalSort = function(a, b) {
|
||||
if (typeof(a) === "string") {
|
||||
a = splitStringToTextAndNumbers(a);
|
||||
}
|
||||
if (typeof(b) === "string") {
|
||||
b = splitStringToTextAndNumbers(b);
|
||||
}
|
||||
|
||||
var comp = lexicographicCompare(a, b);
|
||||
return comp;
|
||||
};
|
||||
|
||||
var isSubpath = function(child, parentPath) {
|
||||
if (!child || !parentPath || child.length <= parentPath.length) { return false; }
|
||||
for (var i = 0; i < parentPath.length; i++) {
|
||||
if (child[i] !== parentPath[i]) { return false; }
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var shouldBeOpened = function(path, openFolders, currentPath) {
|
||||
if (openFolders && openFolders.length) {
|
||||
for (var i = 0; i < openFolders.length; i++) {
|
||||
var openPath = openFolders[i];
|
||||
if (JSON.stringify(openPath) === JSON.stringify(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Auto-expand parent folders of current path
|
||||
if (currentPath && isSubpath(currentPath, path) && path.length < currentPath.length) {
|
||||
if (currentPath.length > 0 && currentPath.length - path.length > 1) { // only auto-expand if currentPath is at least 2 levels deeper than the folder path
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
UIElements.createTreeElement = function (name, $icon, path, draggable, droppable, collapsable, active, events, openFolders, currentPath, cb) {
|
||||
events = events || {};
|
||||
openFolders = openFolders || [];
|
||||
currentPath = currentPath || null;
|
||||
cb = cb || {};
|
||||
var $expandIcon = $(Icons.get('chevron-right'));
|
||||
var $expandedIcon = $(Icons.get('chevron-down'));
|
||||
|
||||
var $name = $('<span>', { 'class': 'cp-app-drive-element' }).text(name);
|
||||
var $collapse;
|
||||
if (collapsable) {
|
||||
$collapse = $('<span>').attr('tabindex', 0).attr('class', 'cp-app-drive-icon-expcol').append($expandIcon.clone());
|
||||
}
|
||||
var $elementRow = $('<span>', {
|
||||
'class': 'cp-app-drive-element-row cp-app-drive-element-folder',
|
||||
'tabindex': 0
|
||||
}).append($collapse).append($icon).append($name).on('click keypress', function (e) {
|
||||
if (e.type === 'keypress' && e.which !== 13) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
if (events.onFolderClicked) {
|
||||
events.onFolderClicked(path, e);
|
||||
}
|
||||
});
|
||||
|
||||
if (events.onContextMenu) {
|
||||
$elementRow.on('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
events.onContextMenu(path, e);
|
||||
});
|
||||
}
|
||||
|
||||
var $element = $('<li>').append($elementRow);
|
||||
if (draggable) {
|
||||
$elementRow.attr('draggable', true);
|
||||
}
|
||||
if (collapsable) {
|
||||
$element.addClass('cp-app-drive-element-collapsed');
|
||||
$collapse.attr({
|
||||
'aria-expanded': 'false',
|
||||
'role': 'button',
|
||||
'aria-label': Messages.ui_expand
|
||||
});
|
||||
$collapse.on('click keypress', function(e) {
|
||||
if (e.type === 'keypress' && e.which !== 13) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
if ($element.hasClass('cp-app-drive-element-collapsed')) {
|
||||
$element.removeClass('cp-app-drive-element-collapsed');
|
||||
$collapse.empty().append($expandedIcon.clone());
|
||||
$collapse.attr('aria-expanded', 'true');
|
||||
$collapse.attr('aria-label', Messages.ui_collapse);
|
||||
if (events.onFolderExpanded) {
|
||||
events.onFolderExpanded(path, true);
|
||||
}
|
||||
} else {
|
||||
$element.addClass('cp-app-drive-element-collapsed');
|
||||
$collapse.empty().append($expandIcon.clone());
|
||||
$collapse.attr('aria-expanded', 'false');
|
||||
$collapse.attr('aria-label', Messages.ui_expand);
|
||||
if (events.onFolderExpanded) {
|
||||
events.onFolderExpanded(path, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Auto-expand if explicitly marked as opened
|
||||
var shouldExpand = shouldBeOpened(path, openFolders, currentPath);
|
||||
|
||||
if (shouldExpand) {
|
||||
$element.removeClass('cp-app-drive-element-collapsed');
|
||||
$collapse.empty().append($expandedIcon.clone());
|
||||
$collapse.attr('aria-expanded', 'true');
|
||||
$collapse.attr('aria-label', Messages.ui_collapse);
|
||||
}
|
||||
}
|
||||
$elementRow.data('path', path);
|
||||
if (typeof cb.addDragAndDropHandlers === 'function') {
|
||||
cb.addDragAndDropHandlers($elementRow, path, true, droppable);
|
||||
}
|
||||
if (active) {
|
||||
$elementRow.addClass('cp-app-drive-element-active cp-leftside-active');
|
||||
}
|
||||
return $element;
|
||||
};
|
||||
|
||||
UIElements.getTree = function (data, config) {
|
||||
config = config || {};
|
||||
config.events = config.events || {};
|
||||
config.cb = config.cb || {};
|
||||
config.openFolders = config.openFolders || [];
|
||||
config.currentPath = config.currentPath || null;
|
||||
|
||||
var $folderIcon = $(Icons.get('folder'));
|
||||
var $folderOpenedIcon = $(Icons.get('folder-open'));
|
||||
|
||||
|
||||
var createTree = function ($container, folderData, path) {
|
||||
if (!folderData || !folderData.content) { return; }
|
||||
|
||||
var $list = $('<ul>').appendTo($container);
|
||||
var keys = Object.keys(folderData.content).sort(function (a, b) {
|
||||
var nameA = folderData.content[a].name || a;
|
||||
var nameB = folderData.content[b].name || b;
|
||||
return naturalSort(nameA, nameB);
|
||||
});
|
||||
|
||||
keys.forEach(function (key) {
|
||||
var item = folderData.content[key];
|
||||
if (!item) { return; }
|
||||
|
||||
var name = item.name || key;
|
||||
var newPath;
|
||||
if (item.navPath) {
|
||||
newPath = item.navPath;
|
||||
} else {
|
||||
// Fallback: reconstruct from parent path (for backwards compatibility)
|
||||
var p = path.slice();
|
||||
p.push(key);
|
||||
newPath = p;
|
||||
}
|
||||
var $icon;
|
||||
if (item.icon) {
|
||||
if (typeof item.icon === 'string') {
|
||||
$icon = $(Icons.get(item.icon));
|
||||
} else {
|
||||
$icon = $(item.icon);
|
||||
}
|
||||
} else {
|
||||
var shouldShowOpened = shouldBeOpened(newPath, config.openFolders, config.currentPath);
|
||||
$icon = shouldShowOpened ? $folderOpenedIcon.clone() : $folderIcon.clone();
|
||||
}
|
||||
|
||||
var hasSubfolder = item.content && Object.keys(item.content).length > 0;
|
||||
var isActive = item.isActive !== undefined ? item.isActive : (config.currentPath && JSON.stringify(newPath) === JSON.stringify(config.currentPath));
|
||||
var $element = UIElements.createTreeElement(name, $icon.clone(), newPath, true, true, hasSubfolder, isActive, config.events, config.openFolders, config.currentPath, config.cb);
|
||||
$element.appendTo($list);
|
||||
|
||||
if (hasSubfolder) {
|
||||
createTree($element, item, newPath);
|
||||
}
|
||||
});
|
||||
};
|
||||
var content = [];
|
||||
var rootKey = Object.keys(data)[0];
|
||||
var rootData = data[rootKey];
|
||||
|
||||
if (rootData) {
|
||||
var rootName = rootData.name || Messages.fm_rootName;
|
||||
var $rootIcon = rootData.icon ?
|
||||
(typeof rootData.icon === 'string' ? $(Icons.get(rootData.icon)) : $(rootData.icon)) :
|
||||
$(Icons.get('drive'));
|
||||
|
||||
var hasContent = rootData.content && Object.keys(rootData.content).length > 0;
|
||||
var isRootActive = config.currentPath && JSON.stringify([rootKey]) === JSON.stringify(config.currentPath);
|
||||
var $rootElement = UIElements.createTreeElement(rootName, $rootIcon, [rootKey], false, true, hasContent, isRootActive, config.events, config.openFolders, config.currentPath, config.cb);
|
||||
$rootElement.addClass('cp-app-drive-tree-root');
|
||||
|
||||
if (!hasContent) {
|
||||
$rootElement.find('.cp-app-drive-icon-expcol').addClass('cp-icon-hidden').attr('tabindex','-1');
|
||||
}
|
||||
var $rootList = $('<ul>', {'class': 'cp-app-drive-tree-docs'}).append($rootElement);
|
||||
content.push($rootList[0]);
|
||||
|
||||
if (hasContent) {
|
||||
createTree($rootElement, rootData, [rootKey]);
|
||||
}
|
||||
}
|
||||
return h('div.cp-drive-tree', content);
|
||||
};
|
||||
|
||||
|
||||
return UIElements;
|
||||
|
||||
@ -915,6 +915,7 @@ define([
|
||||
href: href,
|
||||
title: data.title,
|
||||
owners: optsPut.owners,
|
||||
attributes: common?.otherPadAttrs || {},
|
||||
path: ['template']
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) { return void cb(obj.error); }
|
||||
@ -1170,7 +1171,7 @@ define([
|
||||
|
||||
// Make sure we also store additionnal data to pin all the channels
|
||||
// (OO and forms)
|
||||
data.attributes = {};
|
||||
data.attributes ||= {};
|
||||
Object.keys(common?.otherPadAttrs || {}).forEach(k => {
|
||||
data.attributes[k] = common.otherPadAttrs[k];
|
||||
});
|
||||
@ -1197,7 +1198,7 @@ define([
|
||||
return;
|
||||
}
|
||||
|
||||
let attributes = {};
|
||||
let attributes = data.attributes || {};
|
||||
nThen(function (waitFor) {
|
||||
if (parsed.hashData.type !== 'pad') { return; }
|
||||
// Set the correct owner and expiration time if we can find them
|
||||
|
||||
@ -447,11 +447,17 @@ define([
|
||||
if (restrictedTags.indexOf(root.nodeName.toUpperCase()) === -1) { return true; }
|
||||
return root.getAttribute && /^(blob\:|\/lib\/pdfjs)/.test(root.getAttribute('src'));
|
||||
};
|
||||
// Remove any iframe with srcdoc attribute
|
||||
var checkSrcDoc = function (root) {
|
||||
if (restrictedTags.indexOf(root.nodeName.toUpperCase()) === -1) { return true; }
|
||||
return !(root.getAttribute && root.getAttribute('srcdoc'));
|
||||
};
|
||||
|
||||
var removeForbiddenTags = function (root) {
|
||||
if (!root) { return; }
|
||||
if (forbiddenTags.indexOf(root.nodeName.toUpperCase()) !== -1) { removeNode(root); }
|
||||
if (!checkSrc(root)) { removeNode(root); }
|
||||
if (!checkSrc(root)) { removeNode(root); }
|
||||
if (!checkSrcDoc(root)) { removeNode(root); }
|
||||
slice(root.children).forEach(removeForbiddenTags);
|
||||
};
|
||||
|
||||
|
||||
@ -115,7 +115,7 @@ define([
|
||||
var $folderIcon = $(Icons.get(folder,{"class": "cp-app-drive-icon-folder cp-app-drive-content-icon"}));
|
||||
var $fileMenuIcon = $(Icons.get('ellipsis-horizontal'));
|
||||
var $folderEmptyIcon = $folderIcon.clone();
|
||||
var $folderOpenedIcon = $(Icons.get('folder-open'));
|
||||
var $folderOpenedIcon = $(Icons.get('folder-open', {"class": "cp-app-drive-icon-folder cp-app-drive-content-icon"}));
|
||||
var $folderOpenedEmptyIcon = $folderOpenedIcon.clone();
|
||||
var $sharedFolderIcon = $(Icons.get('drive-shared-folder', {"class": "cp-app-drive-icon-folder"}));
|
||||
var $sharedFolderOpenedIcon = $(Icons.get('folder-open', {"class": "cp-app-drive-icon-folder"}));
|
||||
@ -124,8 +124,6 @@ define([
|
||||
var $recentIcon = $(Icons.get('drive-recent'));
|
||||
var $trashIcon = $(Icons.get('trash-full'));
|
||||
var $trashEmptyIcon = $(Icons.get('trash-empty'));
|
||||
var $expandIcon = $(Icons.get('expand'));
|
||||
var $expandedIcon = $(Icons.get('collapse'));
|
||||
var $sortAscIcon = $(Icons.get('sort-asc', {'class': 'sortasc'}));
|
||||
var $sortDescIcon = $(Icons.get('sort-desc', {'class': 'sortdesc'}));
|
||||
var $closeIcon = $(Icons.get('close'));
|
||||
@ -194,6 +192,18 @@ define([
|
||||
var stored = JSON.parse(APP.store[LS_OPENED] || '[]');
|
||||
return stored.indexOf(JSON.stringify(path)) !== -1;
|
||||
};
|
||||
LS.getOpenedFolders = function () {
|
||||
var stored = JSON.parse(APP.store[LS_OPENED] || '[]');
|
||||
return stored.map(function(s) {
|
||||
try {
|
||||
return JSON.parse(s);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}).filter(function(p) {
|
||||
return p !== null;
|
||||
});
|
||||
};
|
||||
LS.setFolderOpened = function (path, opened) {
|
||||
var s = JSON.stringify(path);
|
||||
var stored = JSON.parse(APP.store[LS_OPENED] || '[]');
|
||||
@ -501,6 +511,7 @@ define([
|
||||
h("ul.dropdown-menu", getNewPadTypes().map(function (app) {
|
||||
return isAppEnabled(app) ? h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable' + (restricted[app] === 0 ? '.cp-app-disabled' : ''), {
|
||||
'tabindex': '-1',
|
||||
'data-type': app,
|
||||
},[
|
||||
Icons.get(AppConfig.applicationsIcon[app] || 'file'),
|
||||
Messages.type[app]
|
||||
@ -930,7 +941,13 @@ define([
|
||||
var removeInput = function (cancel) {
|
||||
if (!cancel && $('.cp-app-drive-element-row > input').length === 1) {
|
||||
var $input = $('.cp-app-drive-element-row > input');
|
||||
manager.rename($input.data('path'), $input.val(), APP.refresh);
|
||||
manager.rename($input.data('path'), $input.val(), function (err) {
|
||||
if (err && err.error === 'E_DUPLICATE_FOLDER_NAME') {
|
||||
UI.alert(err.message || Messages.fo_unavailableName);
|
||||
return;
|
||||
}
|
||||
APP.refresh();
|
||||
});
|
||||
}
|
||||
$('.cp-app-drive-element-row > input').remove();
|
||||
$('.cp-app-drive-element-row > span:hidden').removeAttr('style');
|
||||
@ -945,7 +962,7 @@ define([
|
||||
// Replace a file/folder name by an input to change its value
|
||||
var displayRenameInput = function ($element, path) {
|
||||
// NOTE: setTimeout(f, 0) otherwise the "rename" button in the toolbar is not working
|
||||
window.setTimeout(function () {
|
||||
var renameInput = function () {
|
||||
if (!APP.editable) { return; }
|
||||
if (!path || path.length < 2) {
|
||||
logError("Renaming a top level element (root, trash or filesData) is forbidden.");
|
||||
@ -978,7 +995,11 @@ define([
|
||||
removeInput(true);
|
||||
var newName = $input.val();
|
||||
if (JSON.stringify(path) === JSON.stringify(currentPath)) {
|
||||
manager.rename(path, $input.val(), function () {
|
||||
manager.rename(path, $input.val(), function (err) {
|
||||
if (err && err.error === 'E_DUPLICATE_FOLDER_NAME') {
|
||||
UI.alert(err.message || Messages.fo_unavailableName);
|
||||
return;
|
||||
}
|
||||
if (isFolder) {
|
||||
LS.renameFoldersOpened(path, newName);
|
||||
path[path.length - 1] = newName;
|
||||
@ -987,7 +1008,11 @@ define([
|
||||
});
|
||||
}
|
||||
else {
|
||||
manager.rename(path, $input.val(), function () {
|
||||
manager.rename(path, $input.val(), function (err) {
|
||||
if (err && err.error === 'E_DUPLICATE_FOLDER_NAME') {
|
||||
UI.alert(err.message || Messages.fo_unavailableName);
|
||||
return;
|
||||
}
|
||||
if (isFolder) {
|
||||
LS.renameFoldersOpened(path, newName);
|
||||
unselectElement($element);
|
||||
@ -1032,7 +1057,12 @@ define([
|
||||
e.stopPropagation();
|
||||
$input.parents('.cp-app-drive-element-row').attr("draggable", true);
|
||||
});
|
||||
},0);
|
||||
};
|
||||
if (APP.mobile()) {
|
||||
renameInput();
|
||||
} else {
|
||||
window.setTimeout(renameInput, 0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1851,7 +1881,15 @@ define([
|
||||
if (manager.isPathIn(newPath, [TRASH]) && paths.length && paths[0][0] === TRASH) {
|
||||
return;
|
||||
}
|
||||
var newCb = function () {
|
||||
var newCb = function (err) {
|
||||
if (err && err.error === 'E_DUPLICATE_FOLDER_NAME') {
|
||||
UI.alert(err.message || Messages.fo_unavailableName);
|
||||
paths.forEach(function (path) {
|
||||
LS.moveFoldersOpened(path, newPath);
|
||||
});
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
paths.forEach(function (path) {
|
||||
LS.moveFoldersOpened(path, newPath);
|
||||
});
|
||||
@ -1888,10 +1926,17 @@ define([
|
||||
var onDrag = function (ev, path) {
|
||||
var paths = [];
|
||||
var $element = findDataHolder($(ev.target));
|
||||
var getRefPath = function(navPath) {
|
||||
if (navPath && navPath.length > 1 && navPath[navPath.length - 1] === ROOT) {
|
||||
return navPath.slice(0, -1);
|
||||
}
|
||||
return navPath;
|
||||
};
|
||||
if ($element.hasClass('cp-app-drive-element-selected')) {
|
||||
var $selected = findSelectedElements();
|
||||
$selected.each(function (idx, elmt) {
|
||||
var ePath = $(elmt).data('path');
|
||||
var navPath = $(elmt).data('path');
|
||||
var ePath = getRefPath(navPath);
|
||||
if (ePath) {
|
||||
var val = manager.find(ePath);
|
||||
if (!val) { return; } // Error? A ".selected" element is not in the object
|
||||
@ -1907,12 +1952,13 @@ define([
|
||||
} else {
|
||||
removeSelected();
|
||||
selectElement($element);
|
||||
var val = manager.find(path);
|
||||
var dragPath = getRefPath(path);
|
||||
var val = manager.find(dragPath);
|
||||
if (!val) { return; } // The element is not in the object
|
||||
paths = [{
|
||||
path: path,
|
||||
path: dragPath,
|
||||
value: {
|
||||
name: getElementName(path),
|
||||
name: getElementName(dragPath),
|
||||
el: val
|
||||
}
|
||||
}];
|
||||
@ -2074,6 +2120,18 @@ define([
|
||||
return void UI.warn(Messages.fm_forbidden);
|
||||
}
|
||||
|
||||
if (newPath[0] === TEMPLATE) {
|
||||
var oldPaths = data && JSON.parse(data).path;
|
||||
if (oldPaths && oldPaths.length) {
|
||||
var hasFolder = oldPaths.some(function(p) {
|
||||
var el = manager.find(p.path);
|
||||
return manager.isFolder(el);
|
||||
});
|
||||
if (hasFolder) {
|
||||
return void UI.warn(Messages.fo_moveUnsortedError);
|
||||
}
|
||||
}
|
||||
}
|
||||
var fileDrop = ev.dataTransfer.items;
|
||||
if (fileDrop.length) {
|
||||
// Filter out all the folders and use the correct function to upload them
|
||||
@ -2106,26 +2164,46 @@ define([
|
||||
}
|
||||
}
|
||||
|
||||
var oldPaths = data && JSON.parse(data).path;
|
||||
oldPaths = data && JSON.parse(data).path;
|
||||
if (!oldPaths) { return; }
|
||||
// A moved element should be removed from its previous location
|
||||
var movedPaths = [];
|
||||
|
||||
var sharedF = false;
|
||||
var draggedSharedFolder = false;
|
||||
var folderContainsSharedFolder = false;
|
||||
oldPaths.forEach(function (p) {
|
||||
movedPaths.push(p.path);
|
||||
if (!sharedF && manager.isInSharedFolder(p.path)) {
|
||||
sharedF = true;
|
||||
}
|
||||
if (!draggedSharedFolder && p.value && p.value.el && manager.isSharedFolder(p.value.el)) {
|
||||
draggedSharedFolder = true;
|
||||
}
|
||||
if (!folderContainsSharedFolder && p.value && p.value.el && manager.hasSubSharedFolder(p.value.el)) {
|
||||
folderContainsSharedFolder = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (draggedSharedFolder && manager.isInSharedFolder(newPath)) {
|
||||
return void UI.warn(Messages.fo_sharedFolder_move_error);
|
||||
}
|
||||
if (manager.isPathIn(newPath, [SHARED_FOLDER])) {
|
||||
return void UI.warn(Messages.fo_sharedFolder_move_error);
|
||||
}
|
||||
if (sharedF && manager.isPathIn(newPath, [TRASH])) {
|
||||
// TODO create a key here?
|
||||
// You can't move to YOUR trash documents stored in a shared folder
|
||||
// TODO or keep deletePaths: trigger the "Remove from cryptdrive" modal
|
||||
return void UI.warn(Messages.error);
|
||||
return void UI.warn(Messages.fo_trash_move_error);
|
||||
//return void deletePaths(null, movedPaths);
|
||||
}
|
||||
if (folderContainsSharedFolder) {
|
||||
var destEl = manager.find(newPath);
|
||||
if (manager.isInSharedFolder(newPath) || (destEl && manager.hasSubSharedFolder(destEl))) {
|
||||
return void UI.warn(Messages.fo_sharedFolder_move_error);
|
||||
}
|
||||
}
|
||||
|
||||
var copy = false;
|
||||
if (ev.ctrlKey || (ev.metaKey && APP.isMac)) {
|
||||
@ -3357,13 +3435,11 @@ define([
|
||||
var SORT_FILE_BY = 'sortFilesBy';
|
||||
var SORT_FILE_DESC = 'sortFilesDesc';
|
||||
|
||||
var getSortFolderDesc = function () { // this way the icon change is triggered
|
||||
var v = APP.store[SORT_FOLDER_DESC];
|
||||
return v === true;
|
||||
};
|
||||
var getSortFileDesc = function () {
|
||||
var v = APP.store[SORT_FILE_DESC];
|
||||
return v === true;
|
||||
return APP.store[SORT_FILE_DESC]+"" === "true";
|
||||
};
|
||||
var getSortFolderDesc = function () {
|
||||
return APP.store[SORT_FOLDER_DESC]+"" === "true";
|
||||
};
|
||||
|
||||
var onSortByClick = function () {
|
||||
@ -4349,7 +4425,9 @@ define([
|
||||
if (!root) { return; }
|
||||
}
|
||||
if (!isVirtual && typeof(root) === "undefined") {
|
||||
log(Messages.fm_unknownFolderError);
|
||||
if (!force) {
|
||||
log(Messages.fm_unknownFolderError);
|
||||
}
|
||||
debug("Unable to locate the selected directory: ", path);
|
||||
if (path.length === 1 && path[0] === ROOT) {
|
||||
// Somehow we can't display ROOT. We should abort now because we'll
|
||||
@ -4516,7 +4594,7 @@ define([
|
||||
|
||||
appStatus.ready(true);
|
||||
};
|
||||
var displayDirectory = APP.displayDirectory = function (path, force, cb, opt) {
|
||||
APP.displayDirectory = function (path, force, cb, opt) {
|
||||
cb = Util.once(cb || function () {});
|
||||
if (APP.closed || (APP.$content && !$.contains(document.documentElement, APP.$content[0]))) { return; }
|
||||
if (history.isHistoryMode) {
|
||||
@ -4540,8 +4618,9 @@ define([
|
||||
let fpath = currentPath;
|
||||
if (opt?.init && !manager.isInSharedFolder(fpath) && !priv.anonSFHref) {
|
||||
fixTags();
|
||||
_displayDirectory(path, force);
|
||||
cb();
|
||||
// Wait for shared folder metadata
|
||||
// _displayDirectory(path, force);
|
||||
// cb();
|
||||
}
|
||||
updateSharedFolders(sframeChan, manager, files, folders, function () {
|
||||
_displayDirectory(path, force);
|
||||
@ -4551,75 +4630,58 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
var createTreeElement = function (name, $icon, path, draggable, droppable, collapsable, active, isSharedFolder) {
|
||||
var $name = $('<span>', { 'class': 'cp-app-drive-element' }).text(name);
|
||||
$icon.css("color", isSharedFolder ? getFolderColor(path.slice(0, -1)) : getFolderColor(path));
|
||||
var $collapse;
|
||||
if (collapsable) {
|
||||
$collapse = $('<span>').attr('tabindex', 0).attr('class', 'cp-app-drive-icon-expcol').append($expandIcon.clone());
|
||||
}
|
||||
var $elementRow = $('<span>', {
|
||||
'class': 'cp-app-drive-element-row cp-app-drive-element-folder',
|
||||
'tabindex': 0
|
||||
}).append($collapse).append($icon).append($name).on('click keypress', function (e) {
|
||||
if (e.type === 'keypress' && e.which !== 13) {
|
||||
return;
|
||||
// Build tree data recursively
|
||||
var buildTreeData = function (root, path, currentPath) {
|
||||
var data = { content: {} };
|
||||
var keys = Object.keys(root);
|
||||
keys.forEach(function (key) {
|
||||
// Do not display files in the menu
|
||||
if (!manager.isFolder(root[key])) { return; }
|
||||
var newPath = path.slice();
|
||||
newPath.push(key);
|
||||
var isSharedFolder = manager.isSharedFolder(root[key]) && root[key];
|
||||
var sfId = manager.isInSharedFolder(newPath) || (isSharedFolder && root[key]);
|
||||
var $icon, isCurrentFolder, subfolder, folderName = key;
|
||||
var randomKey = Util.uid();
|
||||
var navPath = newPath.slice();
|
||||
if (isSharedFolder) {
|
||||
// Fix path
|
||||
newPath.push(manager.user.userObject.ROOT);
|
||||
isCurrentFolder = manager.comparePath(newPath, currentPath);
|
||||
// Subfolders?
|
||||
var newRoot = Util.find(manager, ['folders', sfId, 'proxy', manager.user.userObject.ROOT]) || {};
|
||||
subfolder = manager.hasSubfolder(newRoot);
|
||||
// Fix name
|
||||
var sfData = manager.getSharedFolderData(sfId);
|
||||
folderName = sfData.title || sfData.lastTitle || Messages.fm_deletedFolder;
|
||||
// Fix icon
|
||||
$icon = isCurrentFolder ? $sharedFolderOpenedIcon : $sharedFolderIcon;
|
||||
isSharedFolder = sfId;
|
||||
} else {
|
||||
var isEmpty = manager.isFolderEmpty(root[key]);
|
||||
subfolder = manager.hasSubfolder(root[key]);
|
||||
isCurrentFolder = manager.comparePath(newPath, currentPath);
|
||||
$icon = isEmpty ?
|
||||
(isCurrentFolder ? $folderOpenedEmptyIcon : $folderEmptyIcon) :
|
||||
(isCurrentFolder ? $folderOpenedIcon : $folderIcon);
|
||||
}
|
||||
e.stopPropagation();
|
||||
if (isSharedFolder && !manager.folders[isSharedFolder]) {
|
||||
UI.warn(Messages.fm_deletedFolder);
|
||||
return;
|
||||
}
|
||||
if (files.restrictedFolders[isSharedFolder]) {
|
||||
UI.warn(Messages.fm_restricted);
|
||||
return;
|
||||
}
|
||||
APP.displayDirectory(path);
|
||||
});
|
||||
if (files.restrictedFolders[isSharedFolder]) {
|
||||
$elementRow.addClass('cp-app-drive-element-restricted');
|
||||
}
|
||||
if (isSharedFolder) {
|
||||
var sfData = manager.getSharedFolderData(isSharedFolder);
|
||||
_addOwnership($elementRow, $(), sfData);
|
||||
}
|
||||
var $element = $('<li>').append($elementRow);
|
||||
if (draggable) { $elementRow.attr('draggable', true); }
|
||||
if (collapsable) {
|
||||
$element.addClass('cp-app-drive-element-collapsed');
|
||||
$collapse.on('click keypress', function(e) {
|
||||
if (e.type === 'keypress' && e.which !== 13) {
|
||||
return;
|
||||
data.content[randomKey] = {
|
||||
name: folderName,
|
||||
icon: $icon,
|
||||
navPath: navPath,
|
||||
isActive: isCurrentFolder,
|
||||
content: {}
|
||||
};
|
||||
if (subfolder) {
|
||||
var subRoot = isSharedFolder ?
|
||||
Util.find(manager, ['folders', sfId, 'proxy', ROOT]) :
|
||||
root[key];
|
||||
if (subRoot) {
|
||||
data.content[randomKey].content = buildTreeData(subRoot, newPath, currentPath).content;
|
||||
}
|
||||
e.stopPropagation();
|
||||
if ($element.hasClass('cp-app-drive-element-collapsed')) {
|
||||
// It is closed, open it
|
||||
$element.removeClass('cp-app-drive-element-collapsed');
|
||||
LS.setFolderOpened(path, true);
|
||||
$collapse.empty().append($expandedIcon.clone());
|
||||
} else {
|
||||
// Collapse the folder
|
||||
$element.addClass('cp-app-drive-element-collapsed');
|
||||
LS.setFolderOpened(path, false);
|
||||
$collapse.empty().append($expandIcon.clone());
|
||||
// Change the current opened folder if it was collapsed
|
||||
if (manager.isSubpath(currentPath, path)) {
|
||||
displayDirectory(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (LS.wasFolderOpened(path) ||
|
||||
(manager.isSubpath(currentPath, path) && path.length < currentPath.length)) {
|
||||
$collapse.click();
|
||||
}
|
||||
}
|
||||
var dataPath = isSharedFolder ? path.slice(0, -1) : path;
|
||||
$elementRow.data('path', dataPath);
|
||||
addDragAndDropHandlers($elementRow, dataPath, true, droppable);
|
||||
if (active) {
|
||||
$elementRow.addClass('cp-app-drive-element-active cp-leftside-active');
|
||||
}
|
||||
return $element;
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
var createTree = function ($container, path) {
|
||||
@ -4636,93 +4698,112 @@ define([
|
||||
rootName = manager.getSharedFolderData(APP.newSharedFolder).title;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// don't try to display what doesn't exist
|
||||
if (!root) { return; }
|
||||
|
||||
// Display the root element in the tree
|
||||
if (isRoot) {
|
||||
var isRootOpened = manager.comparePath(path.slice(), currentPath);
|
||||
var $rootIcon = manager.isFolderEmpty(files[ROOT]) ?
|
||||
(isRootOpened ? $folderOpenedEmptyIcon : $folderEmptyIcon) :
|
||||
(isRootOpened ? $folderOpenedIcon : $folderIcon);
|
||||
var $rootElement = createTreeElement(rootName, $rootIcon.clone(), path.slice(), false, true, true, isRootOpened);
|
||||
if (!manager.hasSubfolder(root)) {
|
||||
$rootElement.find('.cp-app-drive-icon-expcol').addClass('cp-icon-hidden').attr('tabindex','-1');
|
||||
|
||||
// build tree data structure
|
||||
var treeData = buildTreeData(root, path, currentPath);
|
||||
var data = {
|
||||
root: {
|
||||
name: rootName,
|
||||
icon: $(Icons.get('drive')),
|
||||
content: treeData.content
|
||||
}
|
||||
$rootElement.addClass('cp-app-drive-tree-root');
|
||||
$rootElement.find('>.cp-app-drive-element-row')
|
||||
.contextmenu(openContextMenu('tree'));
|
||||
$('<ul>', {'class': 'cp-app-drive-tree-docs'})
|
||||
.append($rootElement).appendTo($container);
|
||||
$container = $rootElement;
|
||||
} else if (manager.isFolderEmpty(root)) { return; }
|
||||
|
||||
// Display root content
|
||||
var $list = $('<ul>').appendTo($container);
|
||||
var keys = Object.keys(root).sort(function (a, b) {
|
||||
var newA = manager.isSharedFolder(root[a]) ?
|
||||
manager.getSharedFolderData(root[a]).title : a;
|
||||
var newB = manager.isSharedFolder(root[b]) ?
|
||||
manager.getSharedFolderData(root[b]).title : b;
|
||||
return naturalSort(newA, newB);
|
||||
};
|
||||
var openFolders = LS.getOpenedFolders();
|
||||
var treeElement = UIElements.getTree(data, {
|
||||
currentPath: currentPath,
|
||||
cb: {
|
||||
addDragAndDropHandlers: addDragAndDropHandlers
|
||||
},
|
||||
events: {
|
||||
onFolderClicked: function (clickedPath) {
|
||||
// Check for shared folder restrictions
|
||||
var sfId = manager.isInSharedFolder(clickedPath);
|
||||
if (sfId) {
|
||||
if (!manager.folders[sfId]) {
|
||||
UI.warn(Messages.fm_deletedFolder);
|
||||
return;
|
||||
}
|
||||
if (files.restrictedFolders[sfId]) {
|
||||
UI.warn(Messages.fm_restricted);
|
||||
return;
|
||||
}
|
||||
}
|
||||
APP.displayDirectory(clickedPath);
|
||||
},
|
||||
onContextMenu: function (clickedPath, event) {
|
||||
openContextMenu('tree')(event, clickedPath);
|
||||
},
|
||||
onFolderExpanded: function (clickedPath, isOpen) {
|
||||
LS.setFolderOpened(clickedPath, isOpen);
|
||||
}
|
||||
},
|
||||
openFolders: openFolders
|
||||
});
|
||||
keys.forEach(function (key) {
|
||||
// Do not display files in the menu
|
||||
if (!manager.isFolder(root[key])) { return; }
|
||||
var newPath = path.slice();
|
||||
newPath.push(key);
|
||||
var isSharedFolder = manager.isSharedFolder(root[key]) && root[key];
|
||||
var sfId = manager.isInSharedFolder(newPath) || (isSharedFolder && root[key]);
|
||||
var $icon, isCurrentFolder, subfolder;
|
||||
|
||||
var $treeElement = $(treeElement);
|
||||
// Drive specific features
|
||||
$treeElement.find('.cp-app-drive-element-row').each(function() {
|
||||
var $row = $(this);
|
||||
var elPath = $row.data('path');
|
||||
if (!elPath || elPath.length === 0) { return; }
|
||||
var pathToCheck = elPath[0] === ROOT ? elPath : [ROOT].concat(elPath);
|
||||
var isSharedFolderPath = pathToCheck.length > 1 && pathToCheck[pathToCheck.length - 1] === ROOT;
|
||||
$row.find('.cp-app-drive-icon-folder').css("color", isSharedFolderPath ? getFolderColor(pathToCheck.slice(0, -1)) : getFolderColor(pathToCheck));
|
||||
|
||||
var element = manager.find(elPath);
|
||||
var isSharedFolder = element && manager.isSharedFolder(element);
|
||||
var sfId = manager.isInSharedFolder(elPath);
|
||||
var f = null;
|
||||
var droppable = true;
|
||||
if (isSharedFolder) {
|
||||
// Fix path
|
||||
newPath.push(manager.user.userObject.ROOT);
|
||||
isCurrentFolder = manager.comparePath(newPath, currentPath);
|
||||
// Subfolders?
|
||||
var newRoot = Util.find(manager, ['folders', sfId, 'proxy', manager.user.userObject.ROOT]) || {};
|
||||
subfolder = manager.hasSubfolder(newRoot);
|
||||
// Fix name
|
||||
sfId = element;
|
||||
f = folders[sfId];
|
||||
var editable = !(f && f.readOnly);
|
||||
droppable = editable;
|
||||
|
||||
$row.addClass('cp-app-drive-element-sharedf');
|
||||
if (!editable) {
|
||||
$row.closest('li').attr('data-ro', true);
|
||||
}
|
||||
var sfData = manager.getSharedFolderData(sfId);
|
||||
key = sfData.title || sfData.lastTitle || Messages.fm_deletedFolder;
|
||||
// Fix icon
|
||||
$icon = isCurrentFolder ? $sharedFolderOpenedIcon : $sharedFolderIcon;
|
||||
isSharedFolder = sfId;
|
||||
} else {
|
||||
var isEmpty = manager.isFolderEmpty(root[key]);
|
||||
subfolder = manager.hasSubfolder(root[key]);
|
||||
isCurrentFolder = manager.comparePath(newPath, currentPath);
|
||||
$icon = isEmpty ?
|
||||
(isCurrentFolder ? $folderOpenedEmptyIcon : $folderEmptyIcon) :
|
||||
(isCurrentFolder ? $folderOpenedIcon : $folderIcon);
|
||||
_addOwnership($row, $(), sfData);
|
||||
if (files.restrictedFolders[sfId]) {
|
||||
$row.addClass('cp-app-drive-element-restricted');
|
||||
}
|
||||
// Update drag/drop handlers if droppable changed for shared folder
|
||||
$row.off('dragover drop dragenter dragleave');
|
||||
addDragAndDropHandlers($row, elPath, true, droppable);
|
||||
} else if (sfId) {
|
||||
f = folders[sfId];
|
||||
editable = !(f && f.readOnly);
|
||||
droppable = editable;
|
||||
if (!editable) {
|
||||
$row.closest('li').attr('data-ro', true);
|
||||
}
|
||||
$row.off('dragover drop dragenter dragleave');
|
||||
addDragAndDropHandlers($row, elPath, true, droppable);
|
||||
}
|
||||
var f = folders[sfId];
|
||||
var editable = !(f && f.readOnly);
|
||||
var $element = createTreeElement(key, $icon.clone(), newPath, true, editable,
|
||||
subfolder, isCurrentFolder, isSharedFolder);
|
||||
$element.appendTo($list);
|
||||
$element.find('>.cp-app-drive-element-row').contextmenu(openContextMenu('tree'));
|
||||
if (isSharedFolder) {
|
||||
$element.find('>.cp-app-drive-element-row')
|
||||
.addClass('cp-app-drive-element-sharedf');
|
||||
}
|
||||
if (sfId && !editable) {
|
||||
$element.attr('data-ro', true);
|
||||
}
|
||||
if (!subfolder) { return; }
|
||||
createTree($element, newPath);
|
||||
});
|
||||
$treeElement.find('.cp-app-drive-element-row').first().removeAttr('draggable');
|
||||
$container.append($treeElement);
|
||||
};
|
||||
|
||||
var createTrash = function ($container, path) {
|
||||
var $icon = manager.isFolderEmpty(files[TRASH]) ? $trashEmptyIcon.clone() : $trashIcon.clone();
|
||||
var isOpened = manager.comparePath(path, currentPath);
|
||||
var $trashElement = createTreeElement(TRASH_NAME, $icon, [TRASH], false, true, false, isOpened);
|
||||
var $trashElement = UIElements.createTreeElement(TRASH_NAME, $icon, [TRASH], false, true, false, isOpened, {
|
||||
onFolderClicked: function (clickedPath) {
|
||||
APP.displayDirectory(clickedPath);
|
||||
},
|
||||
onContextMenu: function (clickedPath, event) {
|
||||
openContextMenu('trashtree')(event, clickedPath);
|
||||
}
|
||||
}, [], currentPath, { addDragAndDropHandlers: addDragAndDropHandlers });
|
||||
$trashElement.addClass('cp-app-drive-tree-root');
|
||||
$trashElement.find('>.cp-app-drive-element-row')
|
||||
.contextmenu(openContextMenu('trashtree'));
|
||||
var $trashList = $('<ul>', { 'class': 'cp-app-drive-tree-category' })
|
||||
.append($trashElement);
|
||||
var $trashList = $('<ul>', { 'class': 'cp-app-drive-tree-category' }).append($trashElement);
|
||||
$container.append($trashList);
|
||||
};
|
||||
|
||||
@ -4756,7 +4837,11 @@ define([
|
||||
var options = categories[cat];
|
||||
var $icon = options.$icon.clone();
|
||||
var isOpened = manager.comparePath([cat], currentPath);
|
||||
var $element = createTreeElement(options.name, $icon, [cat], options.draggable, options.droppable, false, isOpened);
|
||||
var $element = UIElements.createTreeElement(options.name, $icon, [cat], false, options.droppable, false, isOpened, {
|
||||
onFolderClicked: function (clickedPath) {
|
||||
APP.displayDirectory(clickedPath);
|
||||
}
|
||||
}, [], currentPath, { addDragAndDropHandlers: addDragAndDropHandlers });
|
||||
$element.addClass('cp-app-drive-tree-root');
|
||||
var $list = $('<ul>', { 'class': 'cp-app-drive-tree-category' }).append($element);
|
||||
$container.append($list);
|
||||
@ -4865,6 +4950,12 @@ define([
|
||||
opts.noExpiration = true;
|
||||
}
|
||||
|
||||
let attr = opts.attributes = {};
|
||||
if (data.rtChannel) { attr.rtChannel = data.rtChannel; }
|
||||
if (data.lastVersion) { attr.lastVersion = data.lastVersion; }
|
||||
if (data.lastCpHash) { attr.lastCpHash = data.lastCpHash; }
|
||||
if (data.answersChannel) { attr.answersChannel = data.answersChannel; }
|
||||
|
||||
Access.getAccessModal(common, opts, cb);
|
||||
};
|
||||
|
||||
@ -5055,9 +5146,15 @@ define([
|
||||
$this.hasClass('cp-app-drive-context-collapseall')) {
|
||||
if (paths.length !== 1) { return; }
|
||||
var opened = $this.hasClass('cp-app-drive-context-expandall');
|
||||
var collapsedPath = paths[0].path;
|
||||
var openRecursive = function (path) {
|
||||
LS.setFolderOpened(path, opened);
|
||||
var folderContent = manager.find(path);
|
||||
if (folderContent && manager.isSharedFolder(folderContent)) {
|
||||
path = path.concat(manager.user.userObject.ROOT);
|
||||
folderContent = manager.find(path);
|
||||
}
|
||||
if (!folderContent) { return; }
|
||||
var subfolders = [];
|
||||
for (var k in folderContent) {
|
||||
if (manager.isFolder(folderContent[k])) {
|
||||
@ -5071,11 +5168,19 @@ define([
|
||||
}
|
||||
subfolders.forEach(function (p) {
|
||||
var subPath = path.concat(p);
|
||||
if (Array.isArray(p) && p.length > 1 && p[p.length - 1] === ROOT) {
|
||||
var sharedFolderPath = subPath.slice(0, -1);
|
||||
LS.setFolderOpened(sharedFolderPath, opened);
|
||||
}
|
||||
openRecursive(subPath);
|
||||
});
|
||||
};
|
||||
openRecursive(paths[0].path);
|
||||
refresh();
|
||||
openRecursive(collapsedPath);
|
||||
if (!opened && currentPath && manager.isSubpath(currentPath, collapsedPath)) {
|
||||
APP.displayDirectory(collapsedPath);
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
else if ($this.hasClass('cp-app-drive-context-download')) {
|
||||
|
||||
@ -18,6 +18,25 @@ define([
|
||||
Messages, nThen, Icons) {
|
||||
var Access = {};
|
||||
|
||||
const getOtherChans = (priv, opts) => {
|
||||
// "attributes" contains the additional channels that the other user
|
||||
// has to store (rtChannel, answersChannels, lastVersion, lastCpHash)
|
||||
// - opts.attributes when access modal is created from the drive
|
||||
// - liveAttr when access modal is created from the document
|
||||
let liveAttr = Util.clone(priv.propChannels || {});
|
||||
delete liveAttr.channel;
|
||||
let attributes = opts.attributes || liveAttr;
|
||||
|
||||
// "otherChan" contains the list of channels that should also receive
|
||||
// the ownership changes
|
||||
let otherChan = [];
|
||||
if (attributes?.answersChannel) { otherChan.push(attributes.answersChannel); }
|
||||
if (attributes?.rtChannel) { otherChan.push(attributes.rtChannel); }
|
||||
if (!otherChan.length) { otherChan = undefined; }
|
||||
|
||||
return { otherChan, attributes };
|
||||
};
|
||||
|
||||
var getOwnersTab = function (Env, data, opts, _cb) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
var common = Env.common;
|
||||
@ -37,13 +56,10 @@ define([
|
||||
var teamOwner = data.teamId;
|
||||
var title = opts.title;
|
||||
|
||||
var p = priv.propChannels;
|
||||
var otherChan;
|
||||
if (p && p.answersChannel) {
|
||||
otherChan = [p.answersChannel];
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
const { attributes, otherChan } = getOtherChans(priv, opts);
|
||||
|
||||
var redrawAll = function () {};
|
||||
|
||||
var addBtn = h('button.btn.btn-primary.cp-access-add', [Icons.get('arrow-left'), Icons.get('arrow-up')]);
|
||||
@ -337,6 +353,7 @@ define([
|
||||
: Messages.error;
|
||||
return void UI.warn(text);
|
||||
}
|
||||
data.attributes = attributes;
|
||||
sframeChan.query('Q_ACCEPT_OWNERSHIP', data, function (err, res) {
|
||||
if (err || (res && res.error)) {
|
||||
return void console.error(err || res.error);
|
||||
@ -354,6 +371,7 @@ define([
|
||||
if (!friend) { return; }
|
||||
common.mailbox.sendTo("ADD_OWNER", {
|
||||
channel: data.channel || priv.channel,
|
||||
attributes,
|
||||
channels: otherChan,
|
||||
href: href,
|
||||
calendar: opts.calendar,
|
||||
@ -440,11 +458,7 @@ define([
|
||||
var allowed = data.allowed || [];
|
||||
var teamOwner = data.teamId;
|
||||
|
||||
var p = priv.propChannels;
|
||||
var otherChan;
|
||||
if (p && p.answersChannel) {
|
||||
otherChan = [p.answersChannel];
|
||||
}
|
||||
const { otherChan } = getOtherChans(priv, opts);
|
||||
|
||||
var redrawAll = function () {};
|
||||
|
||||
@ -854,6 +868,8 @@ define([
|
||||
var metadataMgr = common.getMetadataMgr();
|
||||
var priv = metadataMgr.getPrivateData();
|
||||
|
||||
const { otherChan } = getOtherChans(priv, opts);
|
||||
|
||||
var $div = $(h('div.cp-share-columns'));
|
||||
|
||||
if (priv.offline) {
|
||||
@ -1069,13 +1085,15 @@ define([
|
||||
if (err || (obj && obj.error)) { UI.warn(Messages.error); }
|
||||
});
|
||||
|
||||
// If this is a form wiht a answer channel, delete it too
|
||||
var p = priv.propChannels;
|
||||
if (p && p.answersChannel) {
|
||||
sframeChan.query('Q_DELETE_OWNED', {
|
||||
teamId: typeof(owned) !== "boolean" ? owned : undefined,
|
||||
channel: p.answersChannel
|
||||
}, function () {});
|
||||
// If this is a form with an answer channel or an office
|
||||
// doc with an rt channel, delete it too
|
||||
if (otherChan) {
|
||||
otherChan.forEach(chan => {
|
||||
sframeChan.query('Q_DELETE_OWNED', {
|
||||
teamId: typeof(owned) !== "boolean" ? owned : undefined,
|
||||
channel: chan
|
||||
}, function () {});
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!opts.noEditPassword) { $d.append(h('br')); }
|
||||
|
||||
@ -14,6 +14,8 @@
|
||||
* See usage examples at http://jscolor.com/examples/
|
||||
*/
|
||||
|
||||
// NOTE: custom changes for CryptPad:
|
||||
// - "new Function" code has been commented out for security reasons
|
||||
|
||||
"use strict";
|
||||
|
||||
@ -62,6 +64,7 @@ var jsc = {
|
||||
}
|
||||
|
||||
var opts = {};
|
||||
/*
|
||||
if (optsStr) {
|
||||
try {
|
||||
opts = (new Function ('return (' + optsStr + ')'))();
|
||||
@ -69,6 +72,7 @@ var jsc = {
|
||||
jsc.warn('Error parsing jscolor options: ' + eParseError + ':\n' + optsStr);
|
||||
}
|
||||
}
|
||||
*/
|
||||
targetElm.jscolor = new jsc.jscolor(targetElm, opts);
|
||||
}
|
||||
}
|
||||
@ -685,11 +689,11 @@ var jsc = {
|
||||
dispatchFineChange : function (thisObj) {
|
||||
if (thisObj.onFineChange) {
|
||||
var callback;
|
||||
if (typeof thisObj.onFineChange === 'string') {
|
||||
callback = new Function(thisObj.onFineChange);
|
||||
} else {
|
||||
// if (typeof thisObj.onFineChange === 'string') {
|
||||
// callback = new Function(thisObj.onFineChange);
|
||||
// } else {
|
||||
callback = thisObj.onFineChange;
|
||||
}
|
||||
// }
|
||||
callback(thisObj);
|
||||
}
|
||||
},
|
||||
|
||||
@ -85,19 +85,19 @@ define(['json.sortify'], function (Sortify) {
|
||||
dirty = false;
|
||||
if (lazy || lazyUserStr !== Sortify(meta.user)) {
|
||||
metadataLazyObj = JSON.parse(JSON.stringify(metadataObj));
|
||||
lazyChangeHandlers.forEach(function (f) { f(); });
|
||||
lazyChangeHandlers.slice().forEach(function (f) { f(); });
|
||||
} else {
|
||||
metadataLazyObj.users = JSON.parse(JSON.stringify(mdo));
|
||||
}
|
||||
|
||||
if (metadataObj.title !== rememberedTitle) {
|
||||
rememberedTitle = metadataObj.title;
|
||||
titleChangeHandlers.forEach(function (f) {
|
||||
titleChangeHandlers.slice().forEach(function (f) {
|
||||
f(metadataObj.title, metadataObj.defaultTitle);
|
||||
});
|
||||
}
|
||||
|
||||
changeHandlers.forEach(function (f) { f(); });
|
||||
changeHandlers.slice().forEach(function (f) { f(); });
|
||||
};
|
||||
var change = function (lazy) {
|
||||
dirty = true;
|
||||
@ -136,7 +136,7 @@ define(['json.sortify'], function (Sortify) {
|
||||
members.push(ev);
|
||||
if (!meta.user) { return; }
|
||||
change(false);
|
||||
syncHandlers.forEach(function (f) { f(); });
|
||||
syncHandlers.slice().forEach(function (f) { f(); });
|
||||
});
|
||||
sframeChan.on('EV_RT_LEAVE', function (ev) {
|
||||
var idx = members.indexOf(ev);
|
||||
|
||||
@ -10,14 +10,21 @@ define([
|
||||
'/common/common-ui-elements.js',
|
||||
'/common/common-util.js',
|
||||
'/common/common-constants.js',
|
||||
'/components/marked/marked.min.js',
|
||||
'/customize/messages.js',
|
||||
'/customize/pages.js',
|
||||
'/common/common-icons.js',
|
||||
'tui-date-picker'
|
||||
], function($, h, Hash, UI, UIElements, Util, Constants, Messages, Pages, Icons, DatePicker) {
|
||||
], function($, h, Hash, UI, UIElements, Util, Constants, Marked, Messages, Pages, Icons, DatePicker) {
|
||||
|
||||
var handlers = {};
|
||||
|
||||
var renderer = new Marked.Renderer();
|
||||
Marked.setOptions({
|
||||
renderer: renderer,
|
||||
sanitize: true
|
||||
});
|
||||
|
||||
var defaultDismiss = function(common, data) {
|
||||
return function(e) {
|
||||
if (e) {
|
||||
@ -531,12 +538,33 @@ define([
|
||||
var toShow = text[myLang];
|
||||
// Otherwise, fallback to the default language if it exists
|
||||
if (!toShow && defaultL) { toShow = text[defaultL]; }
|
||||
toShow ||= text['default'];
|
||||
// No translation available, dismiss
|
||||
if (!toShow) { return defaultDismiss(common, data)(); }
|
||||
|
||||
var slice = toShow.length > 200;
|
||||
var unsafe = toShow;
|
||||
toShow = Util.fixHTML(toShow);
|
||||
|
||||
if (content.markdown === true) {
|
||||
toShow = Marked.parse(toShow);
|
||||
slice = false;
|
||||
content.handler = function () {
|
||||
var content = h('div', [
|
||||
h('h4', Messages.broadcast_newCustom),
|
||||
UI.setHTML(h('div.cp-admin-message'), toShow)
|
||||
]);
|
||||
$(content).find('a').click(e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const href = e.target.href || '';
|
||||
if (!/^(http|\/)/.test(href)) { return; }
|
||||
common.openURL(e.target.href);
|
||||
});
|
||||
UI.alert(content);
|
||||
};
|
||||
} else {
|
||||
toShow = Util.fixHTML(toShow);
|
||||
}
|
||||
|
||||
content.getFormatText = function () {
|
||||
if (slice) {
|
||||
@ -553,7 +581,9 @@ define([
|
||||
UI.alert(content);
|
||||
};
|
||||
}
|
||||
if (!content.archived) {
|
||||
if (content.dismiss) {
|
||||
content.dismissHandler = content.dismiss;
|
||||
} else if (!content.archived) {
|
||||
content.dismissHandler = defaultDismiss(common, data);
|
||||
}
|
||||
};
|
||||
|
||||
@ -7,8 +7,9 @@ define([
|
||||
'/common/common-interface.js',
|
||||
'/common/hyperscript.js',
|
||||
'/common/common-icons.js',
|
||||
'/common/common-util.js',
|
||||
|
||||
], function ($, UI, h, Icons) {
|
||||
], function ($, UI, h, Icons, Util) {
|
||||
//var ChainPad = window.ChainPad;
|
||||
var History = {};
|
||||
|
||||
@ -27,94 +28,62 @@ define([
|
||||
var cpIndex = -1;
|
||||
var msgIndex = -1;
|
||||
var ooMessages = {};
|
||||
var msgs;
|
||||
var loading = false;
|
||||
var update = function () {};
|
||||
var currentTime;
|
||||
//Defining position here means it can be passed to the showVersion and share functions
|
||||
var position;
|
||||
//Defining patch here means it can be passed to the snapshot function
|
||||
var patch;
|
||||
var currentVersion;
|
||||
var forward;
|
||||
var revertCheckpoint;
|
||||
|
||||
// Get an array of the checkpoint IDs sorted their patch index
|
||||
var hashes = config.onlyoffice.hashes;
|
||||
var sortedCp = Object.keys(hashes).map(Number).sort(function (a, b) {
|
||||
return hashes[a].index - hashes[b].index;
|
||||
});
|
||||
var id;
|
||||
var sortedCp = Object.keys(hashes).map(Number);
|
||||
|
||||
var getId = function () {
|
||||
var cps = sortedCp.length;
|
||||
id = sortedCp[cps -1] || -1;
|
||||
return id;
|
||||
};
|
||||
|
||||
var endWithCp = sortedCp.length &&
|
||||
config.onlyoffice.lastHash === hashes[sortedCp[sortedCp.length - 1]].hash;
|
||||
|
||||
var fillOO = function (id, messages) {
|
||||
if (!id) { return; }
|
||||
if (ooMessages[id]) { return; }
|
||||
var fillOO = function (messages) {
|
||||
ooMessages = {};
|
||||
ooMessages[id] = messages;
|
||||
update();
|
||||
};
|
||||
|
||||
if (endWithCp) { cpIndex = 0; }
|
||||
|
||||
var $version, $time, $share;
|
||||
var $version, $share;
|
||||
var $hist = $toolbar.find('.cp-toolbar-history');
|
||||
$hist.addClass('cp-smallpatch');
|
||||
$hist.addClass('cp-history-oo');
|
||||
var $bottom = $toolbar.find('.cp-toolbar-bottom');
|
||||
var Messages = common.Messages;
|
||||
|
||||
var getVersion = function () {
|
||||
var major = sortedCp.length - cpIndex;
|
||||
return major + '.' + (msgIndex+1);
|
||||
};
|
||||
var showVersion = function (initial) {
|
||||
var v = getVersion();
|
||||
if (initial) {
|
||||
v = Messages.oo_version_latest;
|
||||
var getVersion = function (position, initial, revert) {
|
||||
let version = (id === -1 || id === 0) ? 0 : id;
|
||||
if (!Object.keys(ooMessages).length) {
|
||||
return '0.0';
|
||||
}
|
||||
$version.text(Messages.oo_version + v);
|
||||
|
||||
var $pos = $hist.find('.cp-history-timeline-pos');
|
||||
var cps = sortedCp.length;
|
||||
var id = sortedCp[cps - cpIndex -1] || -1;
|
||||
if (!ooMessages[id]) { return; }
|
||||
var msgs = ooMessages[id];
|
||||
var p = 100*((msgIndex+1) / (msgs.length));
|
||||
$pos.css('margin-left', p+'%');
|
||||
|
||||
var time = msgs[msgIndex] && msgs[msgIndex].time;
|
||||
currentTime = time;
|
||||
if (time) { $time.text(new Date(time).toLocaleString()); }
|
||||
else { $time.text(''); }
|
||||
if (typeof(position) === "undefined" || position === -1) {
|
||||
position = ooMessages[id]?.length || 0;
|
||||
} else if (msgs?.length === position &&
|
||||
id !== parseInt(Object.keys(hashes)[Object.keys(hashes).length-1]) &&
|
||||
!initial && !revert && $(`[data^="${id+1},"][data*=","]`).length > 1) {
|
||||
version = id+1;
|
||||
position = 0;
|
||||
}
|
||||
return version + '.' + position;
|
||||
};
|
||||
|
||||
// We want to load a checkpoint (or initial state)
|
||||
var loadMoreOOHistory = function () {
|
||||
if (!Array.isArray(sortedCp)) { return void console.error("Wrong type"); }
|
||||
|
||||
var cp = {};
|
||||
|
||||
// Get the checkpoint ID
|
||||
var id = -1;
|
||||
if (cpIndex < sortedCp.length) {
|
||||
id = sortedCp[sortedCp.length - 1 - cpIndex];
|
||||
cp = hashes[id];
|
||||
}
|
||||
var nextId = sortedCp[sortedCp.length - cpIndex];
|
||||
|
||||
// Get the history between "toHash" and "fromHash". This function is using
|
||||
// "getOlderHistory", that's why we start from the more recent hash
|
||||
// and we go back in time to an older hash
|
||||
|
||||
// We need to get all the patches between the current cp hash and the next cp hash
|
||||
|
||||
// Current cp or initial hash (invalid hash ==> initial hash)
|
||||
var toHash = cp.hash || 'NONE';
|
||||
// Next cp or last hash
|
||||
var fromHash = nextId ? hashes[nextId].hash : config.onlyoffice.lastHash;
|
||||
|
||||
msgIndex = -1;
|
||||
|
||||
showVersion();
|
||||
if (ooMessages[id]) {
|
||||
// Cp already loaded: reload OO
|
||||
loading = false;
|
||||
return void config.onCheckpoint(cp);
|
||||
}
|
||||
|
||||
var getMessages = function(fromHash, toHash, callback) {
|
||||
sframeChan.query('Q_GET_HISTORY_RANGE', {
|
||||
channel: config.onlyoffice.channel,
|
||||
lastKnownHash: fromHash,
|
||||
@ -123,20 +92,77 @@ define([
|
||||
if (err) { return void console.error(err); }
|
||||
if (!Array.isArray(data.messages)) { return void console.error('Not an array!'); }
|
||||
|
||||
// The first "cp" in history is the empty doc. It doesn't include the first patch
|
||||
// of the history
|
||||
var initialCp = cpIndex === sortedCp.length || !cp.hash;
|
||||
var isEmptyPatch = function(msg) {
|
||||
return msg?.changes?.length === 2 &&
|
||||
msg.changes.some(c => c.change.includes('64;AgAAA')) &&
|
||||
msg.changes.some(c => c.change.includes('18;BgAAA') || c.change.includes('23;BgAAAD'));
|
||||
};
|
||||
|
||||
var messages = (data.messages || []).slice(initialCp ? 0 : 1);
|
||||
var messages;
|
||||
if (data.messages[0] && hashes[id]?.index > JSON.parse(data.messages?.[0]?.msg).changesIndex+1 ) {
|
||||
messages = [];
|
||||
} else if (config.docType() === 'spreadsheet' && toHash === 'NONE') {
|
||||
messages = (data.messages || []);
|
||||
} else {
|
||||
messages = (data.messages || []).slice(1);
|
||||
}
|
||||
|
||||
if (messages[0] && isEmptyPatch(JSON.parse(messages[0].msg))) {
|
||||
revertCheckpoint = true;
|
||||
messages.splice(0, 1);
|
||||
} else if (messages[1] && isEmptyPatch(JSON.parse(messages[1].msg))) {
|
||||
revertCheckpoint = true;
|
||||
messages.splice(1, 1);
|
||||
} else {
|
||||
revertCheckpoint = false;
|
||||
}
|
||||
|
||||
if (config.debug) { console.log(data.messages); }
|
||||
fillOO(id, messages);
|
||||
id = typeof(id) !== "undefined" ? id : getId();
|
||||
|
||||
fillOO(messages);
|
||||
loading = false;
|
||||
config.onCheckpoint(cp);
|
||||
$share.show();
|
||||
callback(null, messages);
|
||||
});
|
||||
};
|
||||
|
||||
// We want to load a checkpoint (or initial state)
|
||||
var loadMoreOOHistory = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!Array.isArray(sortedCp)) {
|
||||
console.error("Wrong type");
|
||||
return reject();
|
||||
}
|
||||
|
||||
// Get the checkpoint ID
|
||||
id = typeof(id) !== "undefined" ? id : getId();
|
||||
var cp = hashes[id];
|
||||
|
||||
// Get the history between "toHash" and "fromHash". This function is using
|
||||
// "getOlderHistory", that's why we start from the more recent hash
|
||||
// and we go back in time to an older hash
|
||||
|
||||
// We need to get all the patches between the current cp hash and the next cp hash
|
||||
|
||||
var nextId = hashes[id+1] ? hashes[id+1] : undefined;
|
||||
// Current cp or initial hash (invalid hash ==> initial hash)
|
||||
var fromHash = cp?.hash || 'NONE';
|
||||
// Next cp or last hash
|
||||
var toHash = nextId ? nextId.hash : config.onlyoffice.lastHash;
|
||||
|
||||
getMessages(toHash, fromHash, function (err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
loadMoreOOHistory();
|
||||
|
||||
var onClose = function () { config.setHistory(false); };
|
||||
var onRevert = function () {
|
||||
config.onRevert();
|
||||
@ -147,51 +173,341 @@ define([
|
||||
$hist.html('').css('display', 'flex');
|
||||
$bottom.hide();
|
||||
|
||||
var spinner = UI.makeSpinner($hist);
|
||||
spinner.spin();
|
||||
// UI.spinner($hist).get().show();
|
||||
|
||||
var $fastPrev, $fastNext, $next;
|
||||
var $fastPrev, $fastNext, $next, $prev;
|
||||
|
||||
var getId = function () {
|
||||
var cps = sortedCp.length;
|
||||
return sortedCp[cps - cpIndex -1] || -1;
|
||||
};
|
||||
|
||||
update = function () {
|
||||
var cps = sortedCp.length;
|
||||
var updateButtons = function () {
|
||||
$fastPrev.show();
|
||||
$next.show();
|
||||
$prev.show();
|
||||
$fastNext.show();
|
||||
$hist.find('.cp-toolbar-history-next, .cp-toolbar-history-previous')
|
||||
.prop('disabled', '');
|
||||
if (cpIndex >= cps && msgIndex === 0) {
|
||||
|
||||
if ((id === -1 || id === 0) && (ooMessages[id]?.length+1 === Math.abs(msgIndex) || !ooMessages[id]?.length && id === 0)){
|
||||
$prev.prop('disabled', 'disabled');
|
||||
$fastPrev.prop('disabled', 'disabled');
|
||||
}
|
||||
if (cpIndex === 0) {
|
||||
$fastNext.prop('disabled', 'disabled');
|
||||
}
|
||||
var id = getId();
|
||||
var msgs = (ooMessages[id] || []).length;
|
||||
if (msgIndex >= (msgs-1)) {
|
||||
var version = currentVersion.split('.');
|
||||
var hashesLength = Object.keys(hashes).length;
|
||||
|
||||
if (currentVersion === Messages.oo_version_latest || hashesLength === parseInt(version[0]) && ooMessages[id]?.length === parseInt(version[1]) ||
|
||||
hashesLength+1 === id && (msgIndex === -1) && forward ||
|
||||
hashes[hashesLength-1] === id && !ooMessages[id].length && msgIndex === 0) {
|
||||
$next.prop('disabled', 'disabled');
|
||||
$fastNext.prop('disabled', 'disabled');
|
||||
}
|
||||
};
|
||||
|
||||
var next = function () {
|
||||
var id = getId();
|
||||
if (!ooMessages[id]) { loading = false; return; }
|
||||
var msgs = ooMessages[id];
|
||||
msgIndex++;
|
||||
var patch = msgs[msgIndex];
|
||||
if (!patch) { loading = false; return; }
|
||||
config.onPatch(patch);
|
||||
showVersion();
|
||||
var loadingFalse = function () {
|
||||
setTimeout(function () {
|
||||
$('iframe').blur();
|
||||
loading = false;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
var showVersion = function (initial, revert, empty) {
|
||||
$('.cp-history-timeline-pos-oo').remove();
|
||||
$('.cp-history-oo-timeline-pos').removeClass('cp-history-oo-timeline-pos');
|
||||
|
||||
var currentPatch;
|
||||
if (initial) {
|
||||
currentPatch = $('.cp-history-patch').last();
|
||||
currentVersion = getVersion(position, initial);
|
||||
} else if (empty) {
|
||||
currentPatch = $(`[data="${id},0"]`);
|
||||
currentVersion = getVersion(position, initial);
|
||||
} else if ($(`[data="${id},${position}"]`).length) {
|
||||
currentPatch = $(`[data="${id},${position}"]`);
|
||||
currentVersion = getVersion(position, initial, true);
|
||||
} else if (msgs?.length === position &&
|
||||
id !== parseInt(Object.keys(hashes)[Object.keys(hashes).length-1]) ) {
|
||||
currentPatch = $(`[data="${id+1},0"]`);
|
||||
currentVersion = getVersion(position, initial);
|
||||
}
|
||||
|
||||
if (initial || position === msgs?.length && (id === -1 ||
|
||||
id === Object.keys(hashes)[Object.keys(hashes).length-1])) {
|
||||
currentVersion = Messages.oo_version_latest;
|
||||
}
|
||||
|
||||
var patchTime = patch ? new Date(patch.time).toLocaleString() : '';
|
||||
$version.text(Messages.oo_version + currentVersion + ' ' + patchTime);
|
||||
var pos = Icons.get('chevron-down', {'class': 'cp-history-timeline-pos-oo'});
|
||||
$(currentPatch).addClass('cp-history-oo-timeline-pos').append(pos);
|
||||
|
||||
updateButtons();
|
||||
loadingFalse();
|
||||
};
|
||||
|
||||
var displayCheckpointTimeline = function(initial) {
|
||||
var bar = $hist.find('.cp-history-timeline-container');
|
||||
$(bar).addClass('cp-history-timeline-bar').addClass('cp-oohistory-bar-el');
|
||||
|
||||
msgs = ooMessages[id];
|
||||
if (initial) {
|
||||
var snapshotsEl = [];
|
||||
var msgsRev = msgs;
|
||||
} else {
|
||||
snapshotsEl = Array.from($hist.find('.cp-history-snapshots')[0].childNodes);
|
||||
msgsRev = msgs.slice().reverse();
|
||||
}
|
||||
|
||||
var cpNfInner = common.startRealtime(config);
|
||||
var md = Util.clone(cpNfInner.metadataMgr.getMetadata());
|
||||
var snapshots = md.snapshots;
|
||||
|
||||
var patchWidth;
|
||||
var patchDiv;
|
||||
var firstCp = (id === 0) ? true : false;
|
||||
for (var i = 0; i < msgsRev.length; i++) {
|
||||
var msg = msgs[i];
|
||||
if (initial || id === -1) {
|
||||
patchWidth = (1/msgs?.length)*100;
|
||||
} else {
|
||||
patchWidth = (1/(msgs?.length+Array.from($hist.find('.cp-history-snapshots')[0].childNodes).length))*100;
|
||||
}
|
||||
|
||||
patchDiv = h('div.cp-history-patch', {
|
||||
style: 'width:'+patchWidth+'%;',
|
||||
title: new Date(msgsRev[i].time).toLocaleString(),
|
||||
data: [id, msgsRev.indexOf(msg)]
|
||||
});
|
||||
if (initial) {
|
||||
snapshotsEl.push(patchDiv);
|
||||
} else {
|
||||
snapshotsEl.unshift(patchDiv);
|
||||
}
|
||||
if (snapshots) {
|
||||
var match = Object.values(snapshots).find(item => item.time === msg.time);
|
||||
if (match) { $(patchDiv).addClass('cp-history-snapshot').append(Icons.get('snapshot', {title: match.title})); }
|
||||
}
|
||||
}
|
||||
|
||||
var finalpatchDiv = h('div.cp-history-patch', {
|
||||
style: 'width:'+patchWidth+'%; height: 100%; position: relative',
|
||||
title: new Date().toLocaleString(),
|
||||
data: [id, msgs?.length]
|
||||
});
|
||||
if (initial) {
|
||||
snapshotsEl.push(finalpatchDiv);
|
||||
} else {
|
||||
firstCp ? snapshotsEl.splice(msgs?.length, 0, finalpatchDiv) : snapshotsEl.splice(msgs?.length-1, 0, finalpatchDiv);
|
||||
}
|
||||
|
||||
if (!msgsRev.length && !Object.keys(hashes).length || initial && !msgs?.length) {
|
||||
$(finalpatchDiv).css('width', '100%');
|
||||
} else {
|
||||
$(finalpatchDiv).css('width', `${($(snapshotsEl[snapshotsEl.indexOf(finalpatchDiv)+1])?.width()/ $(snapshotsEl[snapshotsEl.indexOf(finalpatchDiv)+1])?.parent().width())*100}%`);
|
||||
patchWidth = ($(snapshotsEl[snapshotsEl.indexOf(finalpatchDiv)+1])?.width()/ $(snapshotsEl[snapshotsEl.indexOf(finalpatchDiv)+1])?.parent().width())*100;
|
||||
}
|
||||
|
||||
var pos = Icons.get('chevron-down', {'class': 'cp-history-timeline-pos-oo'});
|
||||
|
||||
var patches = h('div.cp-history-snapshots.cp-history-snapshots-oo', [
|
||||
snapshotsEl
|
||||
]);
|
||||
$(patches).css('height', '100%');
|
||||
$(patches).css('display', 'flex');
|
||||
|
||||
bar.html('').append([
|
||||
patches
|
||||
]);
|
||||
|
||||
if (snapshotsEl.length === 1) {
|
||||
$('.cp-history-patch').css('width', '100%');
|
||||
}
|
||||
if (!initial) {
|
||||
var finalPatchWidth = patchWidth ? patchWidth : 100/$hist.find('.cp-history-snapshots')[0].childNodes.length;
|
||||
Array.from($hist.find('.cp-history-snapshots')[0].childNodes).forEach(function(patch) {
|
||||
$(patch).css('width', `${finalPatchWidth}%`);
|
||||
});
|
||||
}
|
||||
if (initial) {
|
||||
$('.cp-history-patch').last().addClass('cp-history-oo-timeline-pos').append(pos);
|
||||
}
|
||||
|
||||
$('.cp-history-patch').on('click', function(e) {
|
||||
var patchData = $(e.target).attr('data').split(',');
|
||||
|
||||
var cpNo = parseInt(patchData[0]);
|
||||
var patchNo = parseInt(patchData[1]);
|
||||
id = cpNo;
|
||||
|
||||
loadMoreOOHistory().then(() => {
|
||||
msgs = ooMessages[id];
|
||||
if (cpNo === -1) {
|
||||
var q = msgs.slice(0, patchNo);
|
||||
config.onPatchBack({}, q);
|
||||
patch = msgs[patchNo];
|
||||
position = (patchNo === msgs?.length) ? msgs?.length : msgs.indexOf(patch);
|
||||
msgIndex = position === -1 ? -1 : position - msgs?.length-1;
|
||||
showVersion(false, true);
|
||||
updateButtons();
|
||||
return;
|
||||
} else if (cpNo === 0 && patchNo === 0) {
|
||||
config.onPatchBack({});
|
||||
patch = msgs[0];
|
||||
} else if (!msgs?.length ) {
|
||||
q = msgs.slice(0, patchNo);
|
||||
config.onPatchBack(hashes[cpNo], q);
|
||||
patch = msgs[msgs?.length-1];
|
||||
position = patch ? msgs.indexOf(patch)+1 : 0;
|
||||
msgIndex = position === -1 ? -1 : position - msgs?.length-1;
|
||||
showVersion(false, false, true);
|
||||
updateButtons();
|
||||
return;
|
||||
} else if (patchNo === msgs?.length && msgs?.length < $(`[data^="${id},"][data*=","]`).length) {
|
||||
q = msgs.slice(0, patchNo);
|
||||
config.onPatchBack(hashes[cpNo], q);
|
||||
patch = msgs[msgs?.length-1];
|
||||
position = patch ? msgs.indexOf(patch)+1 : 0;
|
||||
msgIndex = position === -1 ? -1 : position - msgs?.length-1;
|
||||
showVersion(false, true);
|
||||
updateButtons();
|
||||
return;
|
||||
} else {
|
||||
q = msgs.slice(0, patchNo);
|
||||
config.onPatchBack(hashes[cpNo], q);
|
||||
patch = msgs[patchNo];
|
||||
}
|
||||
position = patch ? msgs.indexOf(patch) : 0;
|
||||
msgIndex = position === -1 ? -1 : position - msgs?.length-1;
|
||||
showVersion(false);
|
||||
updateButtons();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var restore;
|
||||
|
||||
var next = async function () {
|
||||
forward = true;
|
||||
msgIndex++;
|
||||
msgs = ooMessages[id];
|
||||
var hasHashes = Object.keys(hashes).length;
|
||||
|
||||
if (hasHashes) {
|
||||
// /Check if the end of the checkpoint has been reached and the next one should be loaded
|
||||
if (msgIndex === 0) {
|
||||
id++;
|
||||
await loadMoreOOHistory();
|
||||
msgs = ooMessages[id];
|
||||
|
||||
// Empty checkpoint (checkpoint created/history restored with no further changes)
|
||||
if (!msgs?.length) {
|
||||
config.loadHistoryCp(hashes[id]);
|
||||
msgs = ooMessages[id];
|
||||
msgIndex = -msgs?.length;
|
||||
patch = msgs[0];
|
||||
position = 0;
|
||||
showVersion(false);
|
||||
return;
|
||||
}
|
||||
//Is the checkpoint the result of restoring history? If yes, we need to load an extra patch
|
||||
if (revertCheckpoint ) {
|
||||
msgIndex = -msgs?.length;
|
||||
config.onPatchBack(hashes[id], [msgs[0]]);
|
||||
position = 1;
|
||||
showVersion(false);
|
||||
} else {
|
||||
msgIndex = -msgs?.length - 1;
|
||||
config.loadHistoryCp(hashes[id]);
|
||||
position = 0;
|
||||
showVersion(false, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!msgs?.length) {
|
||||
position = 0;
|
||||
msgIndex = -1;
|
||||
id++;
|
||||
showVersion(false);
|
||||
return config.loadHistoryCp(hashes[id]);
|
||||
}
|
||||
// Adjust msgIndex after fastPrev
|
||||
if (Math.abs(msgIndex) > msgs?.length) { msgIndex = -msgs?.length; }
|
||||
}
|
||||
else if (msgs?.length + msgIndex === -1) { msgIndex++; }
|
||||
|
||||
patch = msgs[msgs?.length + msgIndex];
|
||||
position = msgs.indexOf(patch) + 1;
|
||||
config.onPatch?.(patch);
|
||||
msgIndex === -1
|
||||
? showVersion(false, true)
|
||||
: showVersion(false);
|
||||
};
|
||||
|
||||
var prev = function () {
|
||||
forward = false;
|
||||
msgs = ooMessages[id];
|
||||
let hasHashes = Object.keys(hashes).length;
|
||||
let cp = hasHashes ? hashes[id] : {};
|
||||
let loadPrevCp = (!msgs?.length) ||
|
||||
(msgs?.length + 1 === Math.abs(msgIndex) && id !== 0) ||
|
||||
(msgs?.length - Math.abs(msgIndex) === -2);
|
||||
var isRevert = revertCheckpoint;
|
||||
|
||||
//Check if the end of the checkpoint has been reached and the previous one should be loaded
|
||||
if (hasHashes && loadPrevCp) {
|
||||
id--;
|
||||
msgIndex = -1;
|
||||
return loadMoreOOHistory().then(() => {
|
||||
msgs = ooMessages[id];
|
||||
//Empty checkpoint - checkpoint saved with no further changes
|
||||
if (!msgs?.length) {
|
||||
msgs = ooMessages[id];
|
||||
config.onPatchBack(hashes[id], msgs.slice(0, msgIndex));
|
||||
msgIndex--;
|
||||
patch = msgs[msgs?.length-1];
|
||||
position = msgs.indexOf(patch);
|
||||
if (!$(`[data="${id},0"]`).length) {
|
||||
displayCheckpointTimeline();
|
||||
}
|
||||
showVersion(false, false, true);
|
||||
return;
|
||||
}
|
||||
cp = hashes[id];
|
||||
var q = msgs.slice(0, msgIndex);
|
||||
patch = msgs[msgs?.length-1];
|
||||
|
||||
//Is the checkpoint the result of restoring history? If yes, we need to load an extra patch
|
||||
if (isRevert) {
|
||||
config.onPatchBack(cp, q);
|
||||
msgIndex--;
|
||||
} else {
|
||||
restore = true;
|
||||
if (!$(`[data="${id},${position}"]`).length) {
|
||||
displayCheckpointTimeline(false, true);
|
||||
}
|
||||
config.onPatchBack(cp, msgs);
|
||||
patch = msgs[msgs?.length-1];
|
||||
position = msgs?.length;
|
||||
}
|
||||
|
||||
//Check if this checkpoint has already been added to the timeline
|
||||
if (!$(`[data="${id},0"]`).length) {
|
||||
displayCheckpointTimeline();
|
||||
}
|
||||
showVersion(false, restore);
|
||||
restore = false;
|
||||
});
|
||||
}
|
||||
var q = msgs.slice(0, msgIndex);
|
||||
config.onPatchBack(cp, q);
|
||||
patch = msgs[msgs?.length + msgIndex];
|
||||
msgIndex--;
|
||||
position = msgs.indexOf(patch);
|
||||
showVersion(false);
|
||||
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
displayCheckpointTimeline(true);
|
||||
showVersion(true);
|
||||
}, "1000");
|
||||
|
||||
// Create the history toolbar
|
||||
var display = function () {
|
||||
@ -206,23 +522,25 @@ define([
|
||||
var _next = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
|
||||
Icons.get('history-next'),
|
||||
]);
|
||||
var _prev = h('button.cp-toolbar-history-previous', { title: Messages.history_prev }, [
|
||||
Icons.get('history-prev')
|
||||
]);
|
||||
$fastPrev = $(fastPrev);
|
||||
$prev = $(_prev);
|
||||
$fastNext = $(fastNext).prop('disabled', 'disabled');
|
||||
$next = $(_next).prop('disabled', 'disabled');
|
||||
|
||||
var pos = Icons.get('chevron-down', {'class': 'cp-history-timeline-pos'});
|
||||
var time = h('div.cp-history-timeline-time');
|
||||
var version = h('div.cp-history-timeline-version');
|
||||
$time = $(time);
|
||||
$version = $(version);
|
||||
var bar;
|
||||
var timeline = h('div.cp-toolbar-history-timeline', [
|
||||
h('div.cp-history-timeline-line', [
|
||||
bar = h('span.cp-history-timeline-container')
|
||||
h('span.cp-history-timeline-container')
|
||||
]),
|
||||
h('div.cp-history-timeline-actions', [
|
||||
h('span.cp-history-timeline-prev', [
|
||||
fastPrev,
|
||||
_prev
|
||||
]),
|
||||
time,
|
||||
version,
|
||||
@ -270,7 +588,6 @@ define([
|
||||
|
||||
$share = $(share);
|
||||
$hist.append([timeline, actions]);
|
||||
$(bar).append(pos);
|
||||
|
||||
var onKeyDown, onKeyUp;
|
||||
var closeUI = function () {
|
||||
@ -286,25 +603,69 @@ define([
|
||||
if (loading) { return; }
|
||||
loading = true;
|
||||
next();
|
||||
update();
|
||||
});
|
||||
// Go to previous checkpoint
|
||||
$prev.click(function () {
|
||||
if (loading) { return; }
|
||||
loading = true;
|
||||
prev();
|
||||
});
|
||||
|
||||
// Go to next checkpoint
|
||||
$fastNext.click(function () {
|
||||
if (loading) { return; }
|
||||
loading = true;
|
||||
cpIndex--;
|
||||
loadMoreOOHistory();
|
||||
update();
|
||||
var msgs = ooMessages[id];
|
||||
if (id < Object.keys(hashes).length && id !== -1) {
|
||||
if (id === -1) {
|
||||
id = 1;
|
||||
} else {
|
||||
id++;
|
||||
}
|
||||
loadMoreOOHistory().then(() => {
|
||||
var cp = hashes[id];
|
||||
config.loadHistoryCp(cp);
|
||||
var msgs = ooMessages[id];
|
||||
msgIndex = -msgs?.length-1;
|
||||
position = 0;
|
||||
showVersion(false);
|
||||
loadingFalse();
|
||||
return;
|
||||
});
|
||||
}
|
||||
else {
|
||||
var cp = hashes[id];
|
||||
msgs = ooMessages[id];
|
||||
msgIndex = -1;
|
||||
config.onPatchBack(cp, msgs);
|
||||
}
|
||||
loadingFalse();
|
||||
position = msgs?.length;
|
||||
showVersion(false);
|
||||
});
|
||||
// Go to next checkpoint
|
||||
|
||||
// Go to previous checkpoint
|
||||
$fastPrev.click(function () {
|
||||
if (loading) { return; }
|
||||
loading = true;
|
||||
if (msgIndex === -1) {
|
||||
cpIndex++;
|
||||
}
|
||||
loadMoreOOHistory();
|
||||
update();
|
||||
if (!ooMessages[id].length || ooMessages[id].length+1 === Math.abs(msgIndex)) {
|
||||
id--;
|
||||
}
|
||||
var cp = hashes[id];
|
||||
config.loadHistoryCp(cp);
|
||||
loadMoreOOHistory().then(() => {
|
||||
var msgs = ooMessages[id];
|
||||
msgIndex = -msgs?.length-1;
|
||||
if (!$(`[data="${id},0"]`).length) {
|
||||
displayCheckpointTimeline();
|
||||
}
|
||||
patch = msgs[msgs?.length-1];
|
||||
position = 0;
|
||||
|
||||
showVersion(false);
|
||||
updateButtons(true);
|
||||
});
|
||||
|
||||
loadingFalse();
|
||||
});
|
||||
onKeyDown = function (e) {
|
||||
var p = function () { e.preventDefault(); };
|
||||
@ -319,7 +680,7 @@ define([
|
||||
// Versioned link
|
||||
$share.click(function () {
|
||||
common.getSframeChannel().event('EV_SHARE_OPEN', {
|
||||
versionHash: getVersion()
|
||||
versionHash: getVersion(position)
|
||||
});
|
||||
});
|
||||
$(snapshot).click(function () {
|
||||
@ -345,13 +706,14 @@ define([
|
||||
onClick: function () {
|
||||
var val = $input.val();
|
||||
if (!val) { return true; }
|
||||
msgs = ooMessages[id];
|
||||
config.makeSnapshot(val, function (err) {
|
||||
if (err) { return; }
|
||||
$input.val('');
|
||||
UI.log(Messages.saved);
|
||||
}, {
|
||||
hash: getVersion(),
|
||||
time: currentTime || 0
|
||||
hash: getVersion(position),
|
||||
time: currentTime || patch && patch.time || 0
|
||||
});
|
||||
},
|
||||
keys: [13],
|
||||
@ -382,8 +744,6 @@ define([
|
||||
|
||||
display();
|
||||
|
||||
showVersion(true);
|
||||
//return void loadMoreOOHistory();
|
||||
};
|
||||
|
||||
return History;
|
||||
|
||||
@ -15,6 +15,7 @@ define([
|
||||
'/common/common-feedback.js',
|
||||
'/common/hyperscript.js',
|
||||
'/api/config',
|
||||
'/customize/application_config.js',
|
||||
'/customize/messages.js',
|
||||
'/components/chainpad/chainpad.dist.js',
|
||||
'/file/file-crypto.js',
|
||||
@ -44,6 +45,7 @@ define([
|
||||
Feedback,
|
||||
h,
|
||||
ApiConfig,
|
||||
AppConfig,
|
||||
Messages,
|
||||
ChainPad,
|
||||
FileCrypto,
|
||||
@ -562,19 +564,7 @@ define([
|
||||
}
|
||||
myUniqueOOId = undefined;
|
||||
setMyId();
|
||||
var editor = getEditor();
|
||||
if (editor) {
|
||||
var app = common.getMetadataMgr().getPrivateData().ooType;
|
||||
var d;
|
||||
if (app === 'doc') {
|
||||
d = editor.GetDocument().Document;
|
||||
} else if (app === 'presentation') {
|
||||
d = editor.GetPresentation().Presentation;
|
||||
}
|
||||
if (d) {
|
||||
APP.oldCursor = d.GetSelectionState();
|
||||
}
|
||||
}
|
||||
|
||||
if (APP.docEditor) { APP.docEditor.destroyEditor(); } // Kill the old editor
|
||||
$('iframe[name="frameEditor"]').after(h('div#cp-app-oo-placeholder-a')).remove();
|
||||
ooLoaded = false;
|
||||
@ -606,7 +596,8 @@ define([
|
||||
blob.name = title || (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type;
|
||||
var data = {
|
||||
hash: (APP.history || APP.template) ? ooChannel.historyLastHash : ooChannel.lastHash,
|
||||
index: (APP.history || APP.template) ? ooChannel.currentIndex : ooChannel.cpIndex
|
||||
index: (APP.history || APP.template) ? ooChannel.currentIndex : ooChannel.cpIndex,
|
||||
time: new Date()
|
||||
};
|
||||
fixSheets();
|
||||
|
||||
@ -732,7 +723,7 @@ define([
|
||||
if (window.sendCredentials) { xhr.withCredentials = true; }
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.onload = function () {
|
||||
if (/^4/.test('' + this.status)) {
|
||||
if (/^[45]/.test('' + this.status)) {
|
||||
reject(this.status);
|
||||
return void console.error('XHR error', this.status);
|
||||
}
|
||||
@ -798,6 +789,7 @@ define([
|
||||
return hashes[a].index - hashes[b].index;
|
||||
});
|
||||
var s = version.split('.');
|
||||
var v = parseInt(s[1]);
|
||||
if (s.length !== 2) { return UI.errorLoadingScreen(Messages.error); }
|
||||
|
||||
var major = Number(s[0]);
|
||||
@ -825,8 +817,7 @@ define([
|
||||
|
||||
// The first "cp" in history is the empty doc. It doesn't include the first patch
|
||||
// of the history
|
||||
var initialCp = major === 0 || !cp.hash;
|
||||
var messages = (data.messages || []).slice(initialCp ? 0 : 1, minor);
|
||||
var messages = data.messages;
|
||||
|
||||
messages.forEach(function (obj) {
|
||||
try { obj.msg = JSON.parse(obj.msg); } catch (e) { console.error(e); }
|
||||
@ -859,7 +850,7 @@ define([
|
||||
|
||||
loadLastDocument(cp)
|
||||
.then(({blob, fileType}) => {
|
||||
ooChannel.queue = messages;
|
||||
ooChannel.queue = messages.slice(1, minor+1);
|
||||
resetData(blob, fileType);
|
||||
UI.removeLoadingScreen();
|
||||
})
|
||||
@ -875,7 +866,7 @@ define([
|
||||
var type = common.getMetadataMgr().getPrivateData().ooType;
|
||||
if (APP.downloadType) { type = APP.downloadType; }
|
||||
var blob = loadInitDocument(type, true);
|
||||
ooChannel.queue = messages;
|
||||
ooChannel.queue = file.doc === 'spreadsheet' ? messages.slice(0, v) : messages.slice(0, v+1);
|
||||
resetData(blob, file);
|
||||
UI.removeLoadingScreen();
|
||||
});
|
||||
@ -930,6 +921,7 @@ define([
|
||||
ooChannel.send(obj.data.msg);
|
||||
ooChannel.lastHash = obj.data.hash;
|
||||
ooChannel.cpIndex++;
|
||||
common.notify();
|
||||
} else {
|
||||
ooChannel.queue.push(obj.data);
|
||||
}
|
||||
@ -1910,6 +1902,9 @@ define([
|
||||
mediasData: mediasData
|
||||
}, function (err, obj) {
|
||||
if (err || !obj || !obj.data) {
|
||||
if (integrationChannel) {
|
||||
integrationChannel.event('EV_INTEGRATION_ERROR', 'X2T_ERROR');
|
||||
}
|
||||
UI.alert(Messages.oo_couldNotConvertDocument, cb);
|
||||
return;
|
||||
}
|
||||
@ -1966,6 +1961,9 @@ define([
|
||||
|
||||
const onError = function() {
|
||||
console.error(arguments);
|
||||
if (integrationChannel) {
|
||||
integrationChannel.event('EV_INTEGRATION_ERROR', 'DOCUMENT_ERROR');
|
||||
}
|
||||
if (APP.isDownload) {
|
||||
var sframeChan = common.getSframeChannel();
|
||||
sframeChan.event('EV_OOIFRAME_DONE', '');
|
||||
@ -2056,24 +2054,8 @@ define([
|
||||
getEditor().asc_setDefaultLanguage(l);
|
||||
}
|
||||
|
||||
if (APP.oldCursor) {
|
||||
var app = common.getMetadataMgr().getPrivateData().ooType;
|
||||
var d;
|
||||
if (app === 'doc') {
|
||||
d = getEditor().GetDocument().Document;
|
||||
} else if (app === 'presentation') {
|
||||
d = getEditor().GetPresentation().Presentation;
|
||||
}
|
||||
if (d) {
|
||||
d.SetSelectionState(APP.oldCursor);
|
||||
d.UpdateSelection();
|
||||
}
|
||||
delete APP.oldCursor;
|
||||
}
|
||||
if (integrationChannel) {
|
||||
APP.onDocumentUnlock = () => {
|
||||
integrationChannel.event('EV_INTEGRATION_READY');
|
||||
};
|
||||
integrationChannel.event('EV_INTEGRATION_READY');
|
||||
}
|
||||
}
|
||||
delete APP.startNew;
|
||||
@ -2379,7 +2361,7 @@ define([
|
||||
if (ec.editorConfig?.customization?.goback) {
|
||||
c.goback.blank = true;
|
||||
}
|
||||
if (!privateData?.integrationConfig?.autosave) {
|
||||
if (typeof(privateData?.integrationConfig?.autosave) !== "number") {
|
||||
c.forcesave = true;
|
||||
}
|
||||
}
|
||||
@ -2942,6 +2924,12 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
}
|
||||
};
|
||||
|
||||
var loadHistoryCp = function (cp, keepQueue) {
|
||||
APP.history = true;
|
||||
APP.stopHistory = false;
|
||||
loadCp(cp, keepQueue);
|
||||
};
|
||||
|
||||
var loadTemplate = function (href, pw, parsed) {
|
||||
APP.history = true;
|
||||
APP.template = true;
|
||||
@ -3151,6 +3139,8 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
// flag only when the checkpoint is ready.
|
||||
APP.stopHistory = true;
|
||||
makeCheckpoint(true);
|
||||
toolbar.setHistory(false);
|
||||
|
||||
};
|
||||
var onPatch = function (patch) {
|
||||
// Patch on the current cp
|
||||
@ -3158,14 +3148,43 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
};
|
||||
var onCheckpoint = function (cp) {
|
||||
// We want to load a checkpoint:
|
||||
loadCp(cp);
|
||||
loadCp(cp, true);
|
||||
};
|
||||
var onPatchBack = function (cp, msgs) {
|
||||
APP.history = true;
|
||||
APP.stopHistory = false;
|
||||
if (msgs) {
|
||||
var msgsFormatted = [];
|
||||
msgs.forEach(function(msg) {
|
||||
var parsedMsg = JSON.parse(msg.msg);
|
||||
|
||||
var formattedMsg = {
|
||||
msg: parsedMsg,
|
||||
hash: msg.serverHash,
|
||||
author: msg.author,
|
||||
time: msg.time
|
||||
};
|
||||
msgsFormatted.push(formattedMsg);
|
||||
});
|
||||
ooChannel.queue = msgsFormatted;
|
||||
setTimeout(function () {
|
||||
loadCp(cp, true);
|
||||
}, 200);
|
||||
} else {
|
||||
loadCp(cp);
|
||||
}
|
||||
};
|
||||
var docType = function() {
|
||||
return APP.ooconfig.documentType;
|
||||
};
|
||||
var setHistoryMode = function (bool) {
|
||||
if (bool) {
|
||||
APP.history = true;
|
||||
toolbar.setHistory(true);
|
||||
try { getEditor().asc_setRestriction(true); } catch (e) {}
|
||||
return;
|
||||
}
|
||||
toolbar.setHistory(false);
|
||||
// Cancel button: redraw from lastCp
|
||||
APP.history = false;
|
||||
ooChannel.queue = [];
|
||||
@ -3219,6 +3238,9 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
text: Messages.historyText,
|
||||
tippy: Messages.historyButton
|
||||
});
|
||||
if (!AppConfig.enableHistory) {
|
||||
$historyButton.css('display', 'none');
|
||||
}
|
||||
|
||||
$historyButton.click(function () {
|
||||
ooChannel.historyLastHash = ooChannel.lastHash;
|
||||
@ -3226,6 +3248,10 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
Feedback.send('OO_HISTORY');
|
||||
var histConfig = {
|
||||
onPatch: onPatch,
|
||||
onPatchBack: onPatchBack,
|
||||
docType: docType,
|
||||
loadCp: loadCp,
|
||||
loadHistoryCp: loadHistoryCp,
|
||||
onCheckpoint: onCheckpoint,
|
||||
onRevert: commit,
|
||||
setHistory: setHistoryMode,
|
||||
@ -3515,6 +3541,22 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
}
|
||||
readOnly = true;
|
||||
}
|
||||
} else if (content && content.version <= 8) {
|
||||
version = 'v8/';
|
||||
APP.migrate = true;
|
||||
// Registedred ~~users~~ editors can start the migration
|
||||
if (common.isLoggedIn() && !readOnly) {
|
||||
content.migration = true;
|
||||
APP.onLocal();
|
||||
} else {
|
||||
msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
|
||||
if (APP.helpMenu) {
|
||||
$(APP.helpMenu.menu).after(msg);
|
||||
} else {
|
||||
$('#cp-app-oo-editor').prepend(msg);
|
||||
}
|
||||
readOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the sheet is locked by an offline user, remove it
|
||||
@ -3700,6 +3742,11 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
||||
cb();
|
||||
});
|
||||
});
|
||||
integrationChannel.on('EV_INTEGRATION_MANUAL_SAVE', function () {
|
||||
integrationSave(function () {
|
||||
console.log('Integration manual save');
|
||||
});
|
||||
});
|
||||
|
||||
/* test button
|
||||
if (!cfg.autosave) {
|
||||
|
||||
@ -74,9 +74,9 @@ define([
|
||||
|
||||
Cryptpad.getPadAttribute('rtChannel', function (err, res) {
|
||||
// If already stored, don't pin it again
|
||||
channels.rtChannel = data.channel;
|
||||
if (res && res === data.channel) { return; }
|
||||
Cryptpad.pinPads([data.channel], function () {
|
||||
channels.rtChannel = data.channel;
|
||||
Cryptpad.setPadAttribute('rtChannel', data.channel, function () {});
|
||||
});
|
||||
});
|
||||
|
||||
@ -662,6 +662,11 @@ define([
|
||||
cb();
|
||||
});
|
||||
});
|
||||
integrationChannel.on('EV_INTEGRATION_MANUAL_SAVE', function () {
|
||||
integrationSave(function () {
|
||||
console.log('Integration manual save');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ define([
|
||||
|
||||
module.uploadFile = function (common, data, cb) {
|
||||
var sframeChan = common.getSframeChannel();
|
||||
sframeChan.query('Q_UPLOAD_FILE', data, cb);
|
||||
sframeChan.query('Q_UPLOAD_FILE', data, cb, {raw: true});
|
||||
};
|
||||
|
||||
module.create = function (common, config) {
|
||||
@ -220,14 +220,13 @@ define([
|
||||
|
||||
file.noStore = config.noStore;
|
||||
try {
|
||||
Util.u8ToBase64(u8, b64 => {
|
||||
file.blob = b64;
|
||||
file.teamId = teamId;
|
||||
common.uploadFile(file, function () {
|
||||
console.log('Upload started...');
|
||||
});
|
||||
file.blob = u8;
|
||||
file.teamId = teamId;
|
||||
common.uploadFile(file, function () {
|
||||
console.log('Upload started...');
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
UI.alert(Messages.upload_serverError);
|
||||
}
|
||||
};
|
||||
|
||||
@ -10,9 +10,10 @@ define([
|
||||
'/common/common-ui-elements.js',
|
||||
'/common/notifications.js',
|
||||
'/common/hyperscript.js',
|
||||
'/customize/application_config.js',
|
||||
'/customize/messages.js',
|
||||
'/common/common-icons.js',
|
||||
], function ($, Util, Hash, UI, UIElements, Notifications, h, Messages, Icons) {
|
||||
], function ($, Util, Hash, UI, UIElements, Notifications, h, AppConfig, Messages, Icons) {
|
||||
var Mailbox = {};
|
||||
|
||||
Mailbox.create = function (Common) {
|
||||
@ -116,6 +117,13 @@ define([
|
||||
}
|
||||
if (typeof(data.content.getFormatText) === "function") {
|
||||
$(notif).find('.cp-notification-content p').html(data.content.getFormatText());
|
||||
$(notif).find('a').click(e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const href = e.target.href || '';
|
||||
if (!/^(http|\/)/.test(href)) { return; }
|
||||
Common.openURL(e.target.href);
|
||||
});
|
||||
if (data.content.autorefresh) {
|
||||
var it = setInterval(function () {
|
||||
if (!data.content.autorefresh) {
|
||||
@ -227,6 +235,45 @@ define([
|
||||
history[data.type].push(data.content);
|
||||
};
|
||||
|
||||
const custom = AppConfig.customBroadcast;
|
||||
if (Array.isArray(custom)) {
|
||||
Common.onAccountOnline(metadataMgr => {
|
||||
const priv = metadataMgr.getPrivateData();
|
||||
const dismissed = priv.settings?.broadcast?.viewed || [];
|
||||
custom.forEach(obj => {
|
||||
const viewed = dismissed.includes(obj.id);
|
||||
if (!obj.filter(metadataMgr)) { return; }
|
||||
if (viewed) { return; }
|
||||
const data = {
|
||||
type: 'broadcast',
|
||||
content: {
|
||||
hash: obj.id,
|
||||
markdown: true,
|
||||
dismiss: () => {
|
||||
let $notif = $(`.cp-notification[data-hash^="${obj.id}"]`);
|
||||
if ($notif.length) { $notif.remove(); }
|
||||
if (!dismissed.includes(obj.id)) {
|
||||
dismissed.push(obj.id);
|
||||
}
|
||||
Common.setAttribute(['broadcast', 'viewed'],
|
||||
dismissed);
|
||||
return true;
|
||||
},
|
||||
msg: {
|
||||
type: 'BROADCAST_CUSTOM',
|
||||
content: {
|
||||
content: obj.msg
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
mailbox.onMessage(data, () => {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
mailbox.dismiss = function (data, cb) {
|
||||
var dataObj = {
|
||||
hash: data.content.hash,
|
||||
|
||||
@ -837,6 +837,10 @@ define([
|
||||
!Utils.PadTypes.isAvailable(parsed.type)) {
|
||||
additionalPriv.disabledApp = true;
|
||||
}
|
||||
if (AppConfig.integrationOnly && !cfg.integration) {
|
||||
additionalPriv.disabledApp = true;
|
||||
}
|
||||
|
||||
if (!Utils.LocalStore.isLoggedIn() &&
|
||||
AppConfig.registeredOnlyTypes.indexOf(parsed.type) !== -1 &&
|
||||
parsed.type !== "file") {
|
||||
@ -1113,6 +1117,7 @@ define([
|
||||
href: data.href,
|
||||
channel: data.channel,
|
||||
title: data.title,
|
||||
attributes: data.attributes,
|
||||
owners: data.metadata ? data.metadata.owners : data.owners,
|
||||
expire: data.metadata ? data.metadata.expire : data.expire,
|
||||
forceSave: true
|
||||
@ -1695,7 +1700,6 @@ define([
|
||||
}
|
||||
});
|
||||
};
|
||||
data.blob = Utils.Util.decodeBase64(data.blob);
|
||||
Files.upload(data, data.noStore, Cryptpad, updateProgress, onComplete, onError, onPending);
|
||||
cb();
|
||||
});
|
||||
@ -2122,6 +2126,11 @@ define([
|
||||
cfg.integrationUtils.onHasUnsavedChanges(obj, cb);
|
||||
}
|
||||
});
|
||||
sframeChan.on('Q_INTEGRATION_ERROR', function (obj) {
|
||||
if (cfg.integrationUtils && cfg.integrationUtils.onError) {
|
||||
cfg.integrationUtils.onError(obj);
|
||||
}
|
||||
});
|
||||
sframeChan.on('Q_INTEGRATION_ON_INSERT_IMAGE', function (data, cb) {
|
||||
if (cfg.integrationUtils && cfg.integrationUtils.onInsertImage) {
|
||||
cfg.integrationUtils.onInsertImage(data, cb);
|
||||
@ -2136,6 +2145,11 @@ define([
|
||||
cfg.integrationUtils.setDownloadAs(format => {
|
||||
sframeChan.event('EV_INTEGRATION_DOWNLOADAS', format);
|
||||
});
|
||||
if (cfg.integrationUtils.setSave) {
|
||||
cfg.integrationUtils.setSave(() => {
|
||||
sframeChan.event('EV_INTEGRATION_MANUAL_SAVE');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -92,6 +92,19 @@ define([
|
||||
funcs.getSframeChannel = function () { return ctx.sframeChan; };
|
||||
funcs.getAppConfig = function () { return AppConfig; };
|
||||
|
||||
funcs.onAccountOnline = function (f) {
|
||||
const metadataMgr = ctx.metadataMgr;
|
||||
const cb = Util.once(f);
|
||||
const todo = () => {
|
||||
const priv = metadataMgr.getPrivateData();
|
||||
if (priv.offline !== false) { return; }
|
||||
cb(metadataMgr);
|
||||
metadataMgr.off('change', todo);
|
||||
};
|
||||
metadataMgr.onChange(todo);
|
||||
todo();
|
||||
};
|
||||
|
||||
funcs.isLoggedIn = function () {
|
||||
return ctx.metadataMgr.getPrivateData().loggedIn;
|
||||
};
|
||||
|
||||
@ -665,8 +665,8 @@
|
||||
"friendRequest_notification": "<b>{0}</b> ви изпрати заявка за контакт",
|
||||
"notifications_empty": "Няма налични известия",
|
||||
"profile_addDescription": "Добавяне на описание",
|
||||
"profile_editDescription": "Редактиране на вашето описание",
|
||||
"profile_addLink": "Добавяне на връзка към сайта си",
|
||||
"profile_editDescription": "Описание на профила",
|
||||
"profile_addLink": "Връзка към вашия сайт",
|
||||
"profile_friendRequestSent": "Изчакваща заявка за контакт...",
|
||||
"isContact": "{0} е един от вашите контакти",
|
||||
"isNotContact": "{0} is <b>not</b> един от вашите контакти",
|
||||
@ -735,21 +735,21 @@
|
||||
"team_inviteButton": "Покани участници",
|
||||
"team_owner": "Собственици",
|
||||
"team_admins": "Администратори",
|
||||
"team_members": "членове",
|
||||
"team_members": "Членове",
|
||||
"sent": "Съобщението е изпратено",
|
||||
"team_pending": "Поканен",
|
||||
"team_deleteButton": "Изтрий",
|
||||
"team_deleteButton": "Изтриване",
|
||||
"team_createLabel": "Създай своя нов екип",
|
||||
"team_rosterKick": "Изгони от екипа",
|
||||
"team_leaveButton": "Напусни този екип",
|
||||
"team_nameTitle": "Име на екипа",
|
||||
"team_nameHint": "Задайте име на екипа",
|
||||
"team_nameHint": "Задаване име на екипа",
|
||||
"team_avatarTitle": "Аватар на екипа",
|
||||
"team_listTitle": "Вашите екипи",
|
||||
"team_listSlot": "Свободен слот за екип",
|
||||
"owner_team_add": "{0} желае вие да бъдете собственик на екип <b>{1}</b>. Приемате ли?",
|
||||
"team_deleteTitle": "Изтриване на екип",
|
||||
"team_deleteHint": "Изтрий екипа и всички документи, притежавани ексклузивно от него.",
|
||||
"team_deleteHint": "Изтриване на екипа и всички документи, които са негови.",
|
||||
"team_invitedToTeam": "{0} Ви покани да се присъедините към техния екип: <b>{1}</b>",
|
||||
"team_cat_general": "Относно",
|
||||
"team_cat_list": "Екипи",
|
||||
@ -767,17 +767,17 @@
|
||||
"teams_table": "Роли",
|
||||
"teams_table_generic": "Роли и разрешения",
|
||||
"teams_table_specific": "Изключения",
|
||||
"teams_table_admins": "Управлявай членове",
|
||||
"teams_table_owners": "Управлявай екип",
|
||||
"teams_table_admins": "Управляване на членове",
|
||||
"teams_table_owners": "Управляване на екип",
|
||||
"teams_table_role": "Роля",
|
||||
"pad_wordCount": "Думи: {0}",
|
||||
"share_copyProfileLink": "Копирай линк за профила",
|
||||
"share_copyProfileLink": "Копиране на връзката към профила",
|
||||
"admin_documentMetadataTitle": "Информация за документа",
|
||||
"admin_documentType": "Тип на документа",
|
||||
"admin_documentSize": "Размер на документа",
|
||||
"admin_documentMetadata": "Текущи метаданни",
|
||||
"admin_documentCreationTime": "Създаден",
|
||||
"admin_currentlyOpen": "В момента оторени",
|
||||
"admin_currentlyOpen": "Отворени в момента",
|
||||
"admin_channelAvailable": "Наличен",
|
||||
"admin_channelArchived": "Архивиран",
|
||||
"admin_documentMetadataPlaceholder": "URL на документа или id",
|
||||
@ -785,14 +785,14 @@
|
||||
"admin_reportContent": "Докладвай съдържание",
|
||||
"og_teamDrive": "Екипен диск",
|
||||
"og_login": "Вход във {0}",
|
||||
"admin_documentConflict": "Архивирай/възстанови",
|
||||
"admin_documentConflict": "Архивиране/възстановяване",
|
||||
"fm_filterBy": "Филтър",
|
||||
"fm_rmFilter": "Премахни филтър",
|
||||
"fm_rmFilter": "Премахване на филтъра",
|
||||
"calendar_rec": "Повтори",
|
||||
"calendar_rec_no": "Веднъж",
|
||||
"calendar_rec_daily": "Дневно",
|
||||
"calendar_rec_edit_all": "Редактирай всички събития",
|
||||
"calendar_rec_edit_from": "Редактирай бъдещи събития",
|
||||
"calendar_rec_edit_all": "Редактиране на всички събития",
|
||||
"calendar_rec_edit_from": "Редактиране на бъдещи събития",
|
||||
"calendar_rec_weekend": "Всекидневно през уикенда",
|
||||
"calendar_rec_edit": "Това е повтарящо се събитие",
|
||||
"calendar_str_filter_day": "Дни: {0}",
|
||||
@ -803,7 +803,7 @@
|
||||
"calendar_str_until": "до {0}",
|
||||
"calendar_rec_freq_monthly": "месеци",
|
||||
"calendar_rec_freq_yearly": "години",
|
||||
"calendar_str_monthday": "на",
|
||||
"calendar_str_monthday": "на {0}",
|
||||
"calendar_rec_until_date": "На",
|
||||
"calendar_rec_until_count": "След",
|
||||
"calendar_rec_until_count2": "пъти",
|
||||
@ -820,14 +820,14 @@
|
||||
"calendar_rec_monthly_nth": "Всеки {0} {1} от месеца",
|
||||
"calendar_nth_last": "последен",
|
||||
"calendar_nth_5": "пети",
|
||||
"calendar_removeNotification": "Премахни напомняне",
|
||||
"calendar_removeNotification": "Премахване на напомнянето",
|
||||
"team_inviteRole": "Първоначална роля",
|
||||
"form_exportJSON": "Експортиране в JSON",
|
||||
"team_cat_members": "Членове",
|
||||
"team_maxTeams": "Всеки потребителски акаунт може да бъде член на {0} екипа.",
|
||||
"team_deleteConfirm": "На път сте да изтриете цялата информация на екипа. Това може да повлияе на достъпа на други членове на екипа до техните данни. Това не може да бъде отменено. Сигурен ли сте, че искате да продължите?",
|
||||
"passwordFaqLink": "Прочети повече за пароли",
|
||||
"admin_documentModifiedTime": "Последно модифициран",
|
||||
"team_deleteConfirm": "На път сте да изтриете цялата информация на екипа. Това може да повлияе на достъпа на другите членове на екипа до данните им. Това не може да бъде отменено. Сигурен ли сте, че искате да продължите?",
|
||||
"passwordFaqLink": "Прочети повече за паролите",
|
||||
"admin_documentModifiedTime": "Последна промяна",
|
||||
"calendar_rec_stop": "Спиране на повтарянето",
|
||||
"calendar_rec_freq_weekly": "седмици",
|
||||
"team_avatarHint": "максимален размер от 500 kB (png, jpg, jpeg, gif)",
|
||||
@ -840,15 +840,15 @@
|
||||
"team_quota": "Лимит на съхранение на твоя екип",
|
||||
"admin_fileCount": "Брой файлове",
|
||||
"admin_planlimit": "Лимит на съхранение",
|
||||
"calendar_rec_yearly_nth": "Всеки {0} {1} от {0}",
|
||||
"calendar_rec_yearly_nth": "Всеки {0} {1} от {2}",
|
||||
"support_notification": "Администратор е отговорил на Вашия сигнал за поддръжка",
|
||||
"team_declineInvitation": "{0} е отказал вашата оферта да се присъедини към екипа: <b>{1}</b>",
|
||||
"properties_passwordSuccessFile": "Паролата бе успешно променена.",
|
||||
"admin_getRawMetadata": "История на метаданните",
|
||||
"calendar_rec_until_no": "Никога",
|
||||
"share_noContactsNotLoggedIn": "Влез или се регистрирай, за да видиш свойте контакти и да добавиш нови.",
|
||||
"share_noContactsNotLoggedIn": "Влез или се регистрирай, за да видиш контактите си и да добавиш нови.",
|
||||
"admin_accountMetadataTitle": "Информация за акаунта",
|
||||
"calendar_rec_edit_one": "Редактирай само това събитие",
|
||||
"calendar_rec_edit_one": "Редактиране само на това събитие",
|
||||
"calendar_list_end": "{0} или {1}",
|
||||
"notifications_cat_all": "Всички",
|
||||
"notifications_cat_friends": "Заявки за контакт",
|
||||
@ -884,5 +884,49 @@
|
||||
"teams_table_generic_edit": "Редактиране: създаване, промяна и изтриване на папки и документи.",
|
||||
"teams_table_specificHint": "Това са стари споделени папки, където преглеждащите, все още, имат разрешение да редактират съществуващи документи. Документите, създадени или копирани в тези папки, ще имат стандартни разрешения.",
|
||||
"teams_table_generic_own": "Управление на екипа: промяна на името и аватара на екипа, добавяне или премахване на собственици, промяна на абонамента за екипа, изтриване на екип.",
|
||||
"share_contactPasswordAlert": "Този елемент е защитен с парола. Тъй като го споделяте с контакт от CryptPad, получателят няма да е необходимо да въвежда паролата."
|
||||
"share_contactPasswordAlert": "Този елемент е защитен с парола. Тъй като го споделяте с контакт от CryptPad, получателят няма да е необходимо да въвежда паролата.",
|
||||
"contacts_mute": "Спри известията от контактите",
|
||||
"contacts_unmute": "Възстанови известията от контактите",
|
||||
"contacts_manageMuted": "Управление на спирането на известията",
|
||||
"contacts_mutedUsers": "Заглушени акаунти",
|
||||
"contacts_muteInfo": "Няма да получавате никакви известия или съобщения от заглушени потребители.<br>Те няма да знаят, че сте ги изключили.. ",
|
||||
"team_inviteLinkTitle": "Създаване на персонализирана покана за този екип",
|
||||
"team_inviteLinkTempName": "Временно име (видимо в списъка с чакащи покани)",
|
||||
"team_inviteLinkSetPassword": "Защитаване на връзката с парола (препоръчително)",
|
||||
"team_inviteLinkNote": "Добавяне на лично съобщение",
|
||||
"team_inviteLinkNoteMsg": "Това съобщение ще се покаже, преди получателят да реши дали да се присъедини към този екип.",
|
||||
"team_inviteLinkLoading": "Създаване на вашата връзка",
|
||||
"team_inviteLinkWarning": "Хората, които имат достъп до тази връзка, ще могат да се присъединят към този екип и да виждат неговото съдържание. Споделяйте я внимателно.",
|
||||
"team_inviteLinkErrorName": "Моля, добавете име на лицето, което каните. Те могат да го променят по-късно. ",
|
||||
"team_inviteLinkCreate": "Създаване на връзка",
|
||||
"team_inviteLinkCopy": "Копиране на връзка",
|
||||
"team_inviteFrom": "От:",
|
||||
"team_inviteFromMsg": "{0} ви е поканил да се присъедините към екипа <b>{1}</b>",
|
||||
"team_invitePleaseLogin": "Моля, влезте в системата или се регистрирайте, за да приемете тази покана.",
|
||||
"team_inviteEnterPassword": "Моля, въведете паролата за поканата, за да продължите.",
|
||||
"team_invitePasswordLoading": "Проверка на паролата за поканата",
|
||||
"team_inviteJoin": "Присъединяване към екипа",
|
||||
"team_inviteTitle": "Покана за присъединяване към екип",
|
||||
"team_inviteGetData": "Зареждане на данни за екипа",
|
||||
"team_cat_link": "Връзка за покана",
|
||||
"team_links": "Връзки за покани",
|
||||
"team_inviteInvalidLinkError": "Тази връзка за покана не е валидна.",
|
||||
"team_inviteLinkError": "Възникна грешка при създаването на връзката.",
|
||||
"burnAfterReading_linkBurnAfterReading": "Преглед и автоматично изтриване след това",
|
||||
"burnAfterReading_warningLink": "Вие сте задали този документ да се самоизтрива. След като получателят посети тази връзка, той ще може да прегледа документа веднъж, преди той да бъде окончателно изтрит.",
|
||||
"burnAfterReading_generateLink": "Натиснете бутона по-долу, за да създадете връзка.",
|
||||
"burnAfterReading_warningAccess": "Този документ ще се самоунищожи. Когато натиснете бутона по-долу, ще видите съдържанието веднъж, преди да бъде изтрито за постоянно. Когато затворите този прозорец, няма да имате достъп до него отново. Ако не сте готови да продължите, можете да затворите този прозорец и да се върнете по-късно.",
|
||||
"burnAfterReading_proceed": "преглеждане и изтриване",
|
||||
"burnAfterReading_warningDeleted": "Този документ е изтрит завинаги. След като затворите този прозорец, няма да имате достъп до него отново.",
|
||||
"oo_invalidFormat": "Този файл не може да бъде импортиран",
|
||||
"oo_importInProgress": "Импортирането е в ход",
|
||||
"oo_exportInProgress": "Експортирането е в ход",
|
||||
"oo_sheetMigration_loading": "Документът ви се актуализира до най-новата версия. Моля, изчакайте приблизително 1 минута.",
|
||||
"oo_sheetMigration_complete": "Налична е актуализирана версия, натиснете OK, за да презаредите.",
|
||||
"oo_sheetMigration_anonymousEditor": "Този документ изисква обновление. Редактирането е забранено за гости, докато не го отвори регистриран потребител.",
|
||||
"imprint": "Правно известие",
|
||||
"settings_cat_security": "Сигурност и поверителност",
|
||||
"settings_safeLinksTitle": "Безопасни връзки",
|
||||
"settings_safeLinksCheckbox": "Активиране на безопасни връзки",
|
||||
"safeLinks_error": "Тази връзка е копирана от адресната лента на браузъра и не предоставя достъп до документа. Моля, използвайте менюто<i> </i><b>Споделяне</b>, за да споделите директно с контактите си или копирайте връзката. <a> Прочетете повече за функцията Безопасни връзки</a>."
|
||||
}
|
||||
|
||||
@ -325,7 +325,7 @@
|
||||
"login_invalUser": "Cal introduir el nom d'usuari",
|
||||
"login_invalPass": "Cal introduir la contrasenya",
|
||||
"login_unhandledError": "Hi ha hagut un error inesperat :(",
|
||||
"register_importRecent": "Importeu documents de la sessió de convidat",
|
||||
"register_importRecent": "Importa els documents de la sessió de convidat",
|
||||
"register_acceptTerms": "Accepto les <a>condicions del servei</a>",
|
||||
"register_passwordsDontMatch": "Les contrasenyes no coincideixen!",
|
||||
"register_passwordTooShort": "Les contrasenyes han de tenir, com a mínim, {0} caràcters.",
|
||||
@ -904,7 +904,7 @@
|
||||
"settings_colortheme_dark": "Fosc",
|
||||
"settings_colortheme_light": "Clar",
|
||||
"settings_colortheme_default": "Valor per defecte del sistema ({0})",
|
||||
"settings_colorthemeHint": "Canvia els colors de CryptPad en aquest dispositiu.",
|
||||
"settings_colorthemeHint": "Canvia els colors del CryptPad en aquest dispositiu.",
|
||||
"settings_colorthemeTitle": "Tema de color",
|
||||
"pad_settings_info": "Paràmetres per defecte per a aquest document. S'aplicaran quan els nous usuaris visitin aquest document.",
|
||||
"pad_settings_width_small": "Mode de pàgina",
|
||||
@ -1146,8 +1146,8 @@
|
||||
"friendRequest_notification": "<b>{0}</b> us ha enviat una sol·licitud de contacte",
|
||||
"notifications_empty": "No hi ha notificacions disponibles",
|
||||
"profile_addDescription": "Afegiu una descripció",
|
||||
"profile_editDescription": "Editeu la descripció",
|
||||
"profile_addLink": "Afegiu un enllaç al vostre lloc web",
|
||||
"profile_editDescription": "Descripció del perfil",
|
||||
"profile_addLink": "Enllaç al vostre lloc web",
|
||||
"profile_info": "Altres usuaris poden trobar el vostre perfil a través del vostre avatar a les llistes d'usuaris de documents.",
|
||||
"profile_friendRequestSent": "Sol·licitud de contacte pendent...",
|
||||
"notification_folderShared": "{0} us ha compartit una carpeta: <b>{1}</b>",
|
||||
@ -1350,7 +1350,7 @@
|
||||
"team_exportButton": "Baixa",
|
||||
"admin_limitUser": "Clau pública de l'usuari",
|
||||
"fm_deletedFolder": "S'ha suprimit la carpeta",
|
||||
"access_destroyPad": "Esborra aquest document o carpeta permanentment",
|
||||
"access_destroyPad": "Destrueix aquest document o carpeta permanentment",
|
||||
"fm_shareFolderPassword": "Protegeix aquesta carpeta amb una contrasenya (opcional)",
|
||||
"loading_state_0": "Interfície de construcció",
|
||||
"loading_state_1": "Carrega la unitat",
|
||||
@ -1816,5 +1816,49 @@
|
||||
"toggleArrows": "Botons de fletxa per a moure elements",
|
||||
"profile_remove_avatar": "Elimina aquest avatar",
|
||||
"moveItemLeft": "Mou l'element a l'esquerra",
|
||||
"admin_logoSize_error": "La mida del logotip és massa gran"
|
||||
"admin_logoSize_error": "La mida del logotip és massa gran",
|
||||
"oo_couldNotConvertDocument": "Ha fallat la conversió del document. Tingueu en compte que els formats que donen els millors resultats en importar i exportar són els de Microsoft Office (.docx, .xlsx, .pptx).",
|
||||
"oo_unstableMigrationWarning": "La importació o exportació d'alguns formats d'OpenDocument (.odt, .odp) és inestable, actualment. Recomanem fer servir altres formats si és possible. El CryptPad provarà ara de fer la millor conversió possible.",
|
||||
"badges_admin": "Administració",
|
||||
"badges_moderator": "Moderació i assistència",
|
||||
"badges_premium": "Subscriptor",
|
||||
"badges_error": "Error de validació",
|
||||
"profile_badges": "Distintiu",
|
||||
"show_password": "Mostra la contrasenya",
|
||||
"hide_password": "Amaga la contrasenya",
|
||||
"page_previous": "Pàgina anterior",
|
||||
"page_next": "Pàgina posterior",
|
||||
"settings_profileHeaderHint": "Trieu què veuen els altres sobre el vostre compte",
|
||||
"contact_mastodon": "Mastodon",
|
||||
"contact_forum": "Fòrum",
|
||||
"settings_profileAvatarLabel": "Avatar",
|
||||
"settings_profileHeaderTitle": "Informació del perfil",
|
||||
"toolbar_text_tools": "Eines de text",
|
||||
"toolbar_show_text_tools": "Mostra les eines de text",
|
||||
"toolbar_hide_text_tools": "Amaga les eines de text",
|
||||
"help_close_button": "Tanca la notificació d'ajuda",
|
||||
"fc_open_formaud": "Obre (com a revisor)",
|
||||
"admin_documentsDeletionTitle": "Arxiva diversos documents",
|
||||
"admin_documentsDeletionHint": "Arxiveu una llista de documents fent servir la seva ID (una ID per línia)",
|
||||
"password_note1": "La contrasenya és la clau secreta que xifra tots els vostres documents.",
|
||||
"password_note2": "Si la perdeu, no hi haurà cap manera de recuperar les dades.",
|
||||
"password_note3": "La contrasenya és la clau secreta que xifra tots els vostres documents i els privilegis d'administració d'aquesta instància.",
|
||||
"computer_note": "Si utilitzeu un equip compartit, {0]recordeu tancar la sessió{1} en acabar. Si només tanqueu el navegador, deixareu el vostre compte exposat.",
|
||||
"import_note": "Per a mantenir els documents que heu creat i/o emmagatzemat sense iniciar la sessió, activeu «Importa els documents de la sessió de convidat».",
|
||||
"admin_creation_note": "Creeu el vostre primer compte d'administració en aquesta pàgina. Els administradors gestionen la configuració de la instància, incloent-hi les quotes d'emmagatzematge, i tenen accés a les eines de moderació.",
|
||||
"forms_switchHorizontalAxe": "Canvia a la disposició horitzontal",
|
||||
"forms_switchVerticalAxe": "Canvia a la disposició vertical",
|
||||
"forms_pollOptionNo": "No",
|
||||
"forms_pollOptionYes": "Sí",
|
||||
"forms_pollOptionMaybe": "Potser",
|
||||
"color0": "Cap color",
|
||||
"color1": "Vermell",
|
||||
"color2": "Taronja",
|
||||
"color3": "Groc",
|
||||
"color4": "Verd",
|
||||
"color5": "Verd fosc",
|
||||
"color6": "Turquesa",
|
||||
"color7": "Blau",
|
||||
"color8": "Porpra",
|
||||
"loading_encrypted": "xifrat d'extrem a extrem"
|
||||
}
|
||||
|
||||
@ -1860,5 +1860,8 @@
|
||||
"password_note3": "Dein Passwort ist der geheime Schlüssel, der alle deine Dokumente und Administratorrechte auf dieser Instanz verschlüsselt.",
|
||||
"computer_note": "Wenn du einen gemeinsam genutzten Computer verwendest, {0}denke daran, dich abzumelden{1}, wenn du fertig bist. Durch bloßes Schließen des Browserfensters bleibt das Konto ungeschützt.",
|
||||
"import_note": "Um die Dokumente zu behalten, die du erstellt und/oder gespeichert hast, ohne eingeloggt zu sein, setze einen Haken bei \"Dokumente aus deiner Gast-Sitzung importieren\".",
|
||||
"admin_creation_note": "Erstelle auf dieser Seite deinen ersten Administrator-Account. Administratoren verwalten die Einstellungen der Instanz einschließlich der Speicherplatzkontingente und haben Zugriff auf die Moderationswerkzeuge."
|
||||
"admin_creation_note": "Erstelle auf dieser Seite deinen ersten Administrator-Account. Administratoren verwalten die Einstellungen der Instanz einschließlich der Speicherplatzkontingente und haben Zugriff auf die Moderationswerkzeuge.",
|
||||
"fo_sharedFolder_move_error": "Geteilte Ordner können nicht in andere geteilte Ordner verschoben werden.",
|
||||
"fo_trash_move_error": "Der Inhalt von geteilten Ordnern kann nicht in den Papierkorb verschoben werden. Verwende stattdessen \"Entfernen\".",
|
||||
"form_poll_required_hint": "Die Beantwortung dieser Umfrage ist erforderlich, du musst mit mindestens einer Option interagieren."
|
||||
}
|
||||
|
||||
@ -1839,5 +1839,26 @@
|
||||
"toolbar_text_tools": "Testu-tresnak",
|
||||
"oo_couldNotConvertDocument": "Dokumentuaren bihurtzeak huts egin du. Kontuan izan Microsoft Office formatuak (.docx, .xlsx, .pptx) direla une honetan inportazio-esportazio emaitzarik onenak ematen dutenak.",
|
||||
"oo_unstableMigrationWarning": "OpenDocument formatuen (.odt, .odp) inportazio-esportazioak une onetan ezegonkorrak dira. Posible bada, beste formatuak erabiltzea gomendatzen dugu. Cryptpad-ek ahalegin onenaren bihurketa egingo du orain.",
|
||||
"admin_documentsDeletionHint": "Artxibatu dokumentu zerrenda bat haien IDa erabiliz (dokumentu ID bakarra lerroko)"
|
||||
"admin_documentsDeletionHint": "Artxibatu dokumentu zerrenda bat haien IDa erabiliz (dokumentu ID bakarra lerroko)",
|
||||
"password_note1": "Zure pasahitza zure dokumentu guztiak zifratzen dituen gako sekretua da.",
|
||||
"password_note2": "Galtzen baduzu ez dago modurik zure datuak berreskuratzeko.",
|
||||
"password_note3": "Zure pasahitza instantzia honetako zure dokumentu eta administratzaile-pribilegio guztiak zifratzen dituen gako sekretua da.",
|
||||
"computer_note": "Sare bateko ordenagailu bat erabiltzen ari bazara, {0}gogoratu saioa amaitzen {1} bukatzean. Nabigatzailearen leihoa ixte hutsak zure kontua agerian uzten du.",
|
||||
"import_note": "Saioa hasi gabe sortu edota gorde dituzun dokumentuak mantentzeko, egin klik \"Inportatu dokumentuak zure gonbidatu saiotik\" botoian.",
|
||||
"admin_creation_note": "Sortu zure lehen administratzaile kontua orrialde honetan. Administratzaileek instantziaren ezarpenak kudeatzen dituzte biltegiratze-kuotak barne, eta moderazio-tresnetarako sarbidea dute.",
|
||||
"forms_switchHorizontalAxe": "Aldatu diseinu horizontalera",
|
||||
"forms_switchVerticalAxe": "Aldatu diseinu bertikalera",
|
||||
"forms_pollOptionNo": "Ez",
|
||||
"forms_pollOptionYes": "Bai",
|
||||
"forms_pollOptionMaybe": "Agian",
|
||||
"color0": "Kolorerik gabe",
|
||||
"color1": "Gorria",
|
||||
"color2": "Laranja",
|
||||
"color3": "Horia",
|
||||
"color4": "Berdea",
|
||||
"color5": "Berde iluna",
|
||||
"color6": "Turkesa",
|
||||
"color7": "Urdina",
|
||||
"color8": "Morea",
|
||||
"loading_encrypted": "muturretik muturrerako zifratzea"
|
||||
}
|
||||
|
||||
@ -1860,5 +1860,8 @@
|
||||
"color6": "Turquoise",
|
||||
"color7": "Bleu",
|
||||
"color8": "Violet",
|
||||
"loading_encrypted": "chiffré de bout en bout"
|
||||
"loading_encrypted": "chiffré de bout en bout",
|
||||
"form_poll_required_hint": "Ce sondage est requis, vous devez interagir avec au moins une option.",
|
||||
"fo_trash_move_error": "Le contenu d'un dossier partagé ne peut pas être déplacé dans la corbeille. Veuillez utiliser \"Supprimer\".",
|
||||
"fo_sharedFolder_move_error": "Un dossier partagé ne peut pas être déplacé dans un autre dossier partagé."
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"code": "Codice",
|
||||
"poll": "Sondaggio",
|
||||
"kanban": "Kanban",
|
||||
"slide": "Presentazione in Markdown",
|
||||
"slide": "Presentazione Markdown",
|
||||
"drive": "CryptDrive",
|
||||
"whiteboard": "Lavagna",
|
||||
"file": "File",
|
||||
@ -108,7 +108,7 @@
|
||||
"printText": "Stampa",
|
||||
"printButton": "Stampa (Invio)",
|
||||
"printButtonTitle2": "Stampa il tuo documento o esportalo in formato PDF",
|
||||
"printOptions": "Opzioni di layout",
|
||||
"printOptions": "Opzioni di impaginazione",
|
||||
"printSlideNumber": "Visualizza il numero della diapositiva",
|
||||
"printDate": "Visualizza la data",
|
||||
"printTitle": "Visualizza il titolo del documento",
|
||||
@ -494,12 +494,12 @@
|
||||
"features_f_apps": "Accedi a tutte le applicazioni",
|
||||
"features": "Funzionalità",
|
||||
"terms": "Termini di servizio",
|
||||
"main_catch_phrase": "Suite di collaborazione<br>criptata end-to-end ed open source",
|
||||
"main_catch_phrase": "Suite di collaborazione<br>criptata end-to-end e open source",
|
||||
"mdToolbar_check": "Elenco delle attività",
|
||||
"mdToolbar_heading": "Titolo",
|
||||
"mdToolbar_strikethrough": "Barrato",
|
||||
"mdToolbar_defaultText": "Inserisci qui il tuo testo",
|
||||
"mdToolbar_button": "Mostra o nascondi la barra degli strumenti di Markdown",
|
||||
"mdToolbar_button": "Mostra o nascondi la barra degli strumenti Markdown",
|
||||
"todo_removeTaskTitle": "Rimuovi questa attività dalla tua lista di cose da fare",
|
||||
"todo_markAsIncompleteTitle": "Contrassegna questa attività come non completata",
|
||||
"todo_markAsCompleteTitle": "Contrassegna questa attività come completata",
|
||||
@ -1824,5 +1824,30 @@
|
||||
"admin_addAdminsAdd": "Promuovi un contatto ad amministratore/trice",
|
||||
"toggleArrows": "Pulsanti freccia per spostare gli elementi",
|
||||
"oo_unstableMigrationWarning": "L'importazione e l'esportazione di alcuni formati OpenDocument (.odt, .odp) è attualmente instabile. Si consiglia di utilizzare altri formati quando possibile. CryptPad tenterà ora una conversione.",
|
||||
"settings_profileHeaderTitle": "Informazioni del profilo"
|
||||
"settings_profileHeaderTitle": "Informazioni del profilo",
|
||||
"skipLink": "Vai al contenuto principale",
|
||||
"fc_open_formaud": "Apri (come revisore/a)",
|
||||
"admin_documentsDeletionTitle": "Archivia più documenti",
|
||||
"admin_documentsDeletionHint": "Archivia un elenco di documenti usando il loro codice identificativo (un codice identificativo per riga)",
|
||||
"password_note1": "La tua password è la chiave segreta che cripta tutti i tuoi documenti.",
|
||||
"password_note2": "Se la perdi non c'è modo di recuperare i tuoi dati.",
|
||||
"password_note3": "La tua password è la chiave segreta che cripta tutti i tuoi documenti e i privilegi di amministrazione di questa istanza.",
|
||||
"computer_note": "Se stai usando un computer condiviso, {0}ricorda di disconnetterti{1} quando hai finito. Chiudere solo la finestra del browser espone il tuo account a rischi di sicurezza.",
|
||||
"import_note": "Per mantenere i documenti creati e/o salvati senza aver effettuato l'accesso, spunta \"Importa i documenti dalla tua sessione ospite\".",
|
||||
"loading_encrypted": "criptato end-to-end",
|
||||
"forms_switchHorizontalAxe": "Passa all'impaginazione orizzontale",
|
||||
"forms_switchVerticalAxe": "Passa all'impaginazione verticale",
|
||||
"forms_pollOptionNo": "No",
|
||||
"forms_pollOptionYes": "Sì",
|
||||
"forms_pollOptionMaybe": "Forse",
|
||||
"color1": "Rosso",
|
||||
"color2": "Arancione",
|
||||
"color3": "Giallo",
|
||||
"color4": "Verde",
|
||||
"color5": "Verde scuro",
|
||||
"color6": "Turchese",
|
||||
"color7": "Blu",
|
||||
"color8": "Viola",
|
||||
"admin_creation_note": "Crea il tuo primo account di amministrazione su questa pagina. Gli amministratori e le amministratrici gestiscono le impostazioni dell'istanza, incluso lo spazio di archiviazione, e hanno accesso agli strumenti di moderazione.",
|
||||
"color0": "Senza colore"
|
||||
}
|
||||
|
||||
@ -1860,5 +1860,8 @@
|
||||
"color6": "Turquoise",
|
||||
"color7": "Blue",
|
||||
"color8": "Purple",
|
||||
"loading_encrypted": "end-to-end encrypted"
|
||||
"loading_encrypted": "end-to-end encrypted",
|
||||
"form_poll_required_hint": "This poll is required, you must interact with at least one option.",
|
||||
"fo_trash_move_error": "Shared folder contents cannot be moved to trash. Use \"Remove\" instead.",
|
||||
"fo_sharedFolder_move_error": "Shared folders cannot be moved inside other shared folders."
|
||||
}
|
||||
|
||||
@ -429,5 +429,8 @@
|
||||
"upgradeAccount": "სატარიფო გეგმის აწევა",
|
||||
"templateSaved": "შაბლონი შენახულია!",
|
||||
"uploadButton": "ფაილების ატვირთვა",
|
||||
"uploadFolderButton": "ატვირთვის საქაღალდე"
|
||||
"uploadFolderButton": "ატვირთვის საქაღალდე",
|
||||
"badges_admin": "ადმინისტრატორი",
|
||||
"badges_premium": "გამომწერი",
|
||||
"profile_badges": "სამკერდე ნიშანი"
|
||||
}
|
||||
|
||||
@ -30,19 +30,19 @@
|
||||
"user_displayName": "Rodomas pavadinimas",
|
||||
"user_rename": "Keisti rodomą pavadinimą",
|
||||
"exportPrompt": "Kaip norėtumėte pavadinti savo failą?",
|
||||
"exportButtonTitle": "Eksportuokite padą į vietinį failą",
|
||||
"exportButtonTitle": "Eksportuokite dokumeną į vietinį failą",
|
||||
"exportButton": "Eksportuoti",
|
||||
"importButton": "Importuoti",
|
||||
"importButtonTitle": "Importuokite padą iš vietinio failo",
|
||||
"pinLimitDrive": "Pasiekėte saugyklos limitą. <br> Negalite sukurti naujų padų.",
|
||||
"pinLimitNotPinned": "Pasiekėte saugyklos limitą. <br> Šis padas nėra saugomas jūsų „CryptDrive“.",
|
||||
"importButtonTitle": "Importuokite dokumentą iš vietinio failo",
|
||||
"pinLimitDrive": "Pasiekėte saugyklos limitą. <br> Negalite sukurti naujų dokumentų.",
|
||||
"pinLimitNotPinned": "Pasiekėte saugyklos limitą. <br> Šis dokumentas nėra saugomas jūsų „CryptDrive“.",
|
||||
"pinLimitReachedAlertNoAccounts": "Pasiekėte saugyklos limitą",
|
||||
"pinLimitReachedAlert": "Pasiekėte saugyklos limitą. Nauji padai nebus saugomi jūsų „CryptDrive“. <br> Norėdami padidinti savo limitą, galite pašalinti padus iš savo „CryptDrive“ arba <a> užsiprenumeruoti </a>premium sąskaitą.",
|
||||
"pinLimitReachedAlert": "Pasiekėte saugyklos limitą. Nauji dokumentai nebus saugomi jūsų „CryptDrive“. <br> Norėdami padidinti savo limitą, galite pašalinti dokumentus iš savo „CryptDrive“ arba <a> užsiprenumeruoti </a>premium sąskaitą.",
|
||||
"pinLimitReached": "Pasiekėte saugyklos limitą",
|
||||
"formattedKB": "{0} KB",
|
||||
"formattedKB": "{0} kB",
|
||||
"formattedGB": "{0} GB",
|
||||
"formattedMB": "{0} MB",
|
||||
"KB": "KB",
|
||||
"KB": "kB",
|
||||
"GB": "GB",
|
||||
"MB": "MB",
|
||||
"storageStatus": "Saugykla: <br> <b> {0} </b> sunaudota iš <b> {1} </b>",
|
||||
@ -54,7 +54,7 @@
|
||||
"viewers": "žiūrovai",
|
||||
"viewer": "žiūrovas",
|
||||
"users": "Vartotojai",
|
||||
"anonymous": "Anoniminis",
|
||||
"anonymous": "Sviečias",
|
||||
"readonly": "Tik skaityti",
|
||||
"errorState": "Kritinė klaida: {0}",
|
||||
"forgotten": "Perkeltas į šiukšliadėžę",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1860,5 +1860,8 @@
|
||||
"color6": "Turkusowy",
|
||||
"color7": "Niebieski",
|
||||
"color8": "Fioletowy",
|
||||
"loading_encrypted": "szyfrowane od końca do końca"
|
||||
"loading_encrypted": "szyfrowane od końca do końca",
|
||||
"form_poll_required_hint": "Ta ankieta jest obowiązkowa, należy wybrać co najmniej jedną opcję.",
|
||||
"fo_trash_move_error": "Zawartości folderów współdzielonych nie można przenieść do kosza. Zamiast tego użyj opcji „Usuń”.",
|
||||
"fo_sharedFolder_move_error": "Folderów współdzielonych nie można przenosić do innych folderów współdzielonych."
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
"upgrade": "Upgrade",
|
||||
"MB": "MB",
|
||||
"GB": "GB",
|
||||
"KB": "KB",
|
||||
"KB": "kB",
|
||||
"formattedMB": "{0} MB",
|
||||
"formattedGB": "{0} GB",
|
||||
"formattedKB": "{0} KB",
|
||||
|
||||
@ -1817,5 +1817,51 @@
|
||||
"admin_listName": "Nume admin",
|
||||
"admin_listHardcoded": "Admin adăugat în config.js. Poate fi șters doar prin modificarea fișierului de configurație.",
|
||||
"profile_remove_avatar": "Șterge acest avatar",
|
||||
"oo_unstableMigrationWarning": "Funcționalitatea de Import și Export pentru unele formate OpenDocument (.odt, .odp) sunt instabile în momentul de față. Recomandăm utilizarea altor formate dacă este posibil. CryptPad va încerca acum să realizeze conversia."
|
||||
"oo_unstableMigrationWarning": "Funcționalitatea de Import și Export pentru unele formate OpenDocument (.odt, .odp) sunt instabile în momentul de față. Recomandăm utilizarea altor formate dacă este posibil. CryptPad va încerca acum să realizeze conversia.",
|
||||
"password_note1": "Parola dumneavoastră este cheia secreta care vă criptează toate documentele.",
|
||||
"password_note2": "Dacă o pierdeți, nu vă putem recupera datele în nici un fel.",
|
||||
"password_note3": "Parola dumneavoastră este cheia secretă care vă criptează toate documentele și privilegiile de administrator pe această instanță.",
|
||||
"computer_note": "Dacă împărțiți computer-ul cu mai multe persoane, {0}țineți minte să vă deconectați{1} când terminați. Dacă doar închideți pagina din browser, contul dumneavoastră rămâne expus.",
|
||||
"import_note": "Pentru a păstra documentele pe care le-ați creat și/sau salvat fără să fiți înregistrat, bifați \"Importă documente din sesiunea de invitat\".",
|
||||
"admin_creation_note": "Creați-vă primul cont de administrator pe această pagină. Administratorii gestionează setările instanței inclusiv setează limite pentru spațiul de stocare, și au acces la instrumente de moderare.",
|
||||
"forms_switchHorizontalAxe": "Schimbă la layout orizontal",
|
||||
"forms_switchVerticalAxe": "Schimbă la layout vertical",
|
||||
"forms_pollOptionNo": "Nu",
|
||||
"forms_pollOptionYes": "Da",
|
||||
"forms_pollOptionMaybe": "Poate",
|
||||
"color0": "Fără culoare",
|
||||
"color1": "Roșu",
|
||||
"color2": "Portocaliu",
|
||||
"color3": "Galben",
|
||||
"color4": "Verde",
|
||||
"color5": "Verde închis",
|
||||
"color6": "Turcoaz",
|
||||
"color7": "Albastru",
|
||||
"color8": "Mov",
|
||||
"loading_encrypted": "criptare end-to-end",
|
||||
"oo_couldNotConvertDocument": "Conversia documentului a eșuat. Momentan, conversia formatelor Microsoft Office (.docx, .xslx, .pptx) produce cele mai bune rezultate la import și export.",
|
||||
"badges_admin": "Administrator",
|
||||
"badges_moderator": "Suport și moderare",
|
||||
"badges_premium": "Abonat",
|
||||
"badges_error": "Eroare de validare",
|
||||
"profile_badges": "Insignă",
|
||||
"show_password": "Arată parola",
|
||||
"hide_password": "Ascunde parola",
|
||||
"page_previous": "Pagina anterioară",
|
||||
"page_next": "Pagina următoare",
|
||||
"settings_profileHeaderHint": "Alege ceea ce ceilalți văd despre contul tău",
|
||||
"contact_mastodon": "Mastodon",
|
||||
"contact_forum": "Forum",
|
||||
"settings_profileAvatarLabel": "Avatar",
|
||||
"settings_profileHeaderTitle": "Informații despre profil",
|
||||
"toolbar_text_tools": "Unelte pentru text",
|
||||
"toolbar_show_text_tools": "Afișează uneltele pentru text",
|
||||
"toolbar_hide_text_tools": "Ascunde uneltele pentru text",
|
||||
"help_close_button": "Închide notificarea de ajutor",
|
||||
"fc_open_formaud": "Deschide (ca auditor)",
|
||||
"admin_documentsDeletionTitle": "Arhivează documente multiple",
|
||||
"admin_documentsDeletionHint": "Arhivează o listă de documente după id (un id per linie)",
|
||||
"form_poll_required_hint": "Această întrebare este obligatorie, trebuie să interacționați cu cel puțin o opțiune.",
|
||||
"fo_trash_move_error": "Conținutul unui dosar distribuit nu poate fi mutat la gunoi. În schimb, folosiți funcția \"Elimină\".",
|
||||
"fo_sharedFolder_move_error": "Dosarele distribuite nu pot fi mutate în alte dosare distribuite."
|
||||
}
|
||||
|
||||
@ -294,7 +294,7 @@
|
||||
"printCSS": "Пользовательские настройки вида (CSS):",
|
||||
"viewEmbedTag": "Чтобы встроить данный документ, вставьте этот iframe в нужную страницу. Вы можете настроить внешний вид используя CSS или атрибуты HTML.",
|
||||
"fm_ownedPadsName": "Собственный",
|
||||
"fm_info_anonymous": "Вы не вошли в учетную запись, поэтому срок действия ваших документов истечет через {0} дней. Они хранятся в вашем браузере, поэтому очистка истории может привести к их исчезновению.<br><a href=\"/register/\">Зарегистрируйтесь</a> или <a href=\"/login/\">Войдите</a> чтобы сохранить их. <a href=\"#docs\">Подробнее о зарегистрированных аккаунтах</a>.",
|
||||
"fm_info_anonymous": "Вы не вошли в учётную запись, поэтому срок действия Ваших документов истечёт через {0} дней. Они хранятся только в Вашем браузере, поэтому очистка истории браузера может привести к их исчезновению.<br><a href=\"/register/\">Зарегистрируйтесь</a> или <a href=\"/login/\">Войдите</a> чтобы сохранить их. <a href=\"#docs\">Подробнее о зарегистрированных аккаунтах</a>.",
|
||||
"fm_burnThisDriveButton": "Удалить всю информацию, хранящуюся от CryptPad в браузере",
|
||||
"fm_tags_used": "Количество использований",
|
||||
"fm_restoreDrive": "Восстановление прежнего состояния диска. Для достижения наилучших результатов не вносите изменения в диск, пока этот процесс не будет завершен.",
|
||||
@ -455,12 +455,12 @@
|
||||
"settings_driveDuplicateHint": "Когда Вы помещаете свои документы в общую папку, копия сохраняется в Вашем CryptDrive, чтобы удостовериться, что Вы всё ещё имеете контроль над ними. Вы можете скрыть продублированные файлы. Будет отображена только публичная версия документа (если не удалена), иначе будет отображена оригинальная версия и место, где ее последнего расположения.",
|
||||
"settings_driveDuplicateTitle": "Дубликаты собственных документов",
|
||||
"settings_logoutEverywhereConfirm": "Вы уверены? Вам придётся войти в аккаунт на всех устройствах.",
|
||||
"settings_autostoreHint": "<b>Автоматически</b> Все открытые вами документы будут сохранены в вашем CryptDrive.<br><b>Вручную (всегда спрашивать)</b> Если вы еще не сохранили документ, вам будет предложено сохранить документ в CryptDrive.<br><b>Вручную (никогда не спрашивать)</b> Документы не будут автоматически сохраняться в вашем CryptDrive. Кнопка для их сохранения будет скрыта.",
|
||||
"settings_autostoreHint": "<b>Автоматически</b> Все открытые Вами документы будут сохранены в Вашем CryptDrive.<br><b>Вручную (всегда спрашивать)</b> Если Вы еще не сохранили документ, Вам будет предложено сохранить документ в CryptDrive.<br><b>Вручную (никогда не спрашивать)</b> Документы не будут автоматически сохраняться в Вашем CryptDrive. Кнопка для их сохранения будет скрыта.",
|
||||
"register_emailWarning3": "Нажмите OK, если осознанно хотите использовать свою почту в качестве имени пользователя.",
|
||||
"register_emailWarning2": "Восстановить ваш пароль с помощью почты, как на других сайтах, не получится.",
|
||||
"register_emailWarning1": "Вы можете сделать это, если хочется, но на наш сервер это отправлено не будет.",
|
||||
"register_emailWarning0": "Похоже, что ваша почта и имя пользователя совпадают.",
|
||||
"password_submit": "Задать",
|
||||
"password_submit": "Готово",
|
||||
"team_pendingOwner": "(ожидание)",
|
||||
"team_deleteButton": "Удалить",
|
||||
"team_pending": "Приглашённые",
|
||||
@ -498,7 +498,7 @@
|
||||
"adminPage": "Администрирование",
|
||||
"admin_cat_stats": "Статистика",
|
||||
"admin_cat_general": "Общее",
|
||||
"autostore_store": "Хранить",
|
||||
"autostore_store": "Скопировать",
|
||||
"autostore_pad": "документ",
|
||||
"autostore_sf": "папка",
|
||||
"autostore_file": "файл",
|
||||
@ -598,7 +598,7 @@
|
||||
"features_f_devices": "Ваши документы на всех устройствах",
|
||||
"features_f_cryptdrive1_note": "Папки, общие папки, шаблоны, теги",
|
||||
"features_f_cryptdrive1": "Полная функциональность CryptDrive",
|
||||
"features_f_anon_note": "С дополнительными функциями",
|
||||
"features_f_anon_note": "С дополнительными возможностями",
|
||||
"features_anon": "Гость",
|
||||
"features_f_anon": "Все функции доступные гостю",
|
||||
"features_f_storage0_note": "Документы удаляются после {0} дней бездействия",
|
||||
@ -706,7 +706,7 @@
|
||||
"redo": "Повторить",
|
||||
"undo": "Отменить",
|
||||
"settings_cacheHint": "CryptPad хранит части ваших документов в памяти вашего браузера, чтобы сократить использование сети и сократить время загрузки. Вы можете отключить кеш, если на вашем устройстве недостаточно свободного места для хранения. По соображениям безопасности кеш всегда очищается при выходе из системы, но вы можете очистить его вручную, если хотите освободить место для хранения на вашем компьютере.",
|
||||
"autostore_notstored": "Этот {0} не в вашем CryptDrive. Вы хотите сохранить его сейчас?",
|
||||
"autostore_notstored": "Этот {0} не в вашем CryptDrive. Хотите скопировать его туда сейчас?",
|
||||
"chrome68": "Похоже, вы используете браузер Chrome или Chromium версии 68. Он содержит ошибку, из-за которой через несколько секунд страница становится полностью белой или страница не реагирует на клики мышью. Чтобы решить эту проблему, вы можете переключиться на другую вкладку и вернуться или попробовать прокрутить страницу. Эта ошибка должна быть исправлена в следующей версии вашего браузера.",
|
||||
"convertFolderToSF_confirm": "Эта папка должна быть преобразована в общую папку, чтобы другие могли ее просматривать. Продолжать?",
|
||||
"convertFolderToSF_SFChildren": "Эту папку нельзя преобразовать в общую папку, поскольку она уже содержит общие папки. Чтобы продолжить, переместите эти общие папки в другое место.",
|
||||
@ -920,7 +920,7 @@
|
||||
"contact_bug": "Сообщить об ошибке",
|
||||
"contact_devHint": "Для запросов новых функций, улучшений удобства использования или для того, чтобы поблагодарить.",
|
||||
"contact_dev": "Связаться с разработчиками",
|
||||
"contact_adminHint": "По любым вопросам, связанным с Вашей учетной записью, лимитом хранилища или доступностью службы.\n",
|
||||
"contact_adminHint": "По любым вопросам, связанным с Вашей учётной записью, лимитом хранилища или доступностью сервиса.\n",
|
||||
"contact_admin": "Связаться с администраторами: {0}",
|
||||
"admin_flushCacheDone": "Кеш успешно очищен",
|
||||
"admin_flushCacheButton": "Очистить кеш",
|
||||
@ -943,8 +943,8 @@
|
||||
"autostore_notAvailable": "Вы должны сохранить этот документ на Вашем CryptDrive, прежде чем сможете использовать эту функцию.",
|
||||
"autostore_forceSave": "Сохраните файл в Вашем CryptDrive",
|
||||
"autostore_saved": "Документ успешно сохранен на вашем CryptDrive!",
|
||||
"autostore_error": "Неожиданная ошибка: нам не удалось сохранить этот документ, пожалуйста попробуйте еще раз.",
|
||||
"autostore_hide": "Не хранить",
|
||||
"autostore_error": "Неожиданная ошибка: нам не удалось скопировать этот документ, пожалуйста попробуйте еще раз.",
|
||||
"autostore_hide": "Не копировать",
|
||||
"autostore_settings": "Вы можете включить автоматическое хранение документов на странице <a>Настройки</a>.",
|
||||
"areYouSure": "Вы уверены?",
|
||||
"settings_safeLinksHint": "CryptPad указывает ключи для расшифровки Ваших документов в ссылках. Любой, у кого есть доступ к Вашей истории просмотров, потенциально может прочитать Ваши данные. Сюда входят подсматривающие расширения для браузеров и браузеры, которые синхронизируют Вашу историю на разных устройствах. Включение «безопасных ссылок» предотвращает попадание ключей в историю просмотров или отображение в адресной строке везде, где это возможно. Мы настоятельно рекомендуем Вам включить эту функцию и использовать меню {0} Поделиться для создания ссылок общего доступа.",
|
||||
@ -1116,8 +1116,8 @@
|
||||
"errorPopupBlocked": "Для работы CryptPad должен иметь возможность открывать новые вкладки. Пожалуйста, разрешите всплывающие окна в адресной строке вашего браузера. Эти окна никогда не будут использоваться для показа рекламы.",
|
||||
"tag_edit": "Изменить",
|
||||
"tag_add": "Добавить",
|
||||
"loading_state_5": "Восстановить документ",
|
||||
"loading_state_4": "Загрузить команды",
|
||||
"loading_state_5": "Восстановление документа",
|
||||
"loading_state_4": "Загрузка Команд",
|
||||
"team_exportHint": "Скачайте все документы с диска этой команды. Документы будут загружаться в форматах, доступных для чтения другими приложениями, если такой формат доступен. Когда такой формат недоступен, документы будут загружены в формате, доступном для чтения CryptPad.",
|
||||
"team_exportTitle": "Скачать диск команды",
|
||||
"admin_limitSetNote": "Заметка",
|
||||
@ -1839,5 +1839,29 @@
|
||||
"help_close_button": "Закрыть уведомление о помощи",
|
||||
"fc_open_formaud": "Открыть (как аудитор)",
|
||||
"admin_documentsDeletionTitle": "Архивировать несколько документов",
|
||||
"admin_documentsDeletionHint": "Архивировать несколько документов, используя их идентификаторы (по одному идентификатору документа в строке)"
|
||||
"admin_documentsDeletionHint": "Архивировать несколько документов, используя их идентификаторы (по одному идентификатору документа в строке)",
|
||||
"password_note1": "Пароль — это секретный ключ, который шифрует все ваши документы.",
|
||||
"password_note2": "Если Вы потеряете его, мы не сможем восстановить Ваши данные.",
|
||||
"password_note3": "Ваш пароль — это секретный ключ, который шифрует все Ваши документы и права администратора в этом экземпляре.",
|
||||
"computer_note": "Если Вы используете общий компьютер, {0}не забудьте выйти из системы{1} когда закончите. Если Вы только закроете окно браузера, Ваш профиль останется незапертым.",
|
||||
"import_note": "Чтобы сохранить документы, которые Вы создали и/или сохранили не входя в систему, установите флажок «Импортировать документы из гостевого сеанса».",
|
||||
"admin_creation_note": "На этой странице создайте свой первый аккаунт администратора. Администраторы управляют настройками экземпляров, включая квоты хранилища, и имеют доступ к инструментам модерации.",
|
||||
"forms_switchHorizontalAxe": "Переключить на горизонтальную компоновку",
|
||||
"forms_switchVerticalAxe": "Переключить на вертикальную компоновку",
|
||||
"forms_pollOptionNo": "Нет",
|
||||
"forms_pollOptionYes": "Да",
|
||||
"forms_pollOptionMaybe": "Возможно",
|
||||
"color0": "Без цвета",
|
||||
"color1": "Красный",
|
||||
"color2": "Оранжевый",
|
||||
"color3": "Жёлтый",
|
||||
"color4": "Зелёный",
|
||||
"color5": "Тёмно-зелёный",
|
||||
"color6": "Бирюзовый",
|
||||
"color7": "Синий",
|
||||
"color8": "Фиолетовый",
|
||||
"loading_encrypted": "с сквозным шифрованием",
|
||||
"form_poll_required_hint": "Этот вопрос обязателен, Вы должны выбрать хотя бы один вариант.",
|
||||
"fo_trash_move_error": "Содержимое общей папки нельзя переместить в корзину. Вместо этого используйте \"Удалить\".",
|
||||
"fo_sharedFolder_move_error": "Общие папки нельзя перемещать внутрь других общих папок."
|
||||
}
|
||||
|
||||
@ -351,8 +351,8 @@
|
||||
"uploadButtonTitle": "将新文件上传到您的 CryptDrive",
|
||||
"uploadFolderButton": "上传文件夹",
|
||||
"uploadButton": "上传文件",
|
||||
"form_required_on": "必要的",
|
||||
"form_required_off": "选项",
|
||||
"form_required_on": "必填",
|
||||
"form_required_off": "选填",
|
||||
"form_preview": "表格预览",
|
||||
"form_changeTypeConfirm": "选择新的问题类型。",
|
||||
"form_corruptAnswers": "此表单已经有回复。 更改此问题类型可能会使以前的响应数据无效。",
|
||||
@ -1839,5 +1839,29 @@
|
||||
"fc_open_formaud": "打开(作为审计者)",
|
||||
"help_close_button": "关闭帮助通知",
|
||||
"admin_documentsDeletionTitle": "归档多个文档",
|
||||
"admin_documentsDeletionHint": "使用文档 ID 归档文档列表(每行一个文档 ID)"
|
||||
"admin_documentsDeletionHint": "使用文档 ID 归档文档列表(每行一个文档 ID)",
|
||||
"forms_pollOptionNo": "否",
|
||||
"forms_pollOptionYes": "是",
|
||||
"forms_pollOptionMaybe": "可能",
|
||||
"color0": "无颜色",
|
||||
"color1": "红色",
|
||||
"color2": "橙色",
|
||||
"color3": "黄色",
|
||||
"color4": "绿色",
|
||||
"color5": "深绿色",
|
||||
"color6": "蓝绿色",
|
||||
"color7": "蓝色",
|
||||
"color8": "紫色",
|
||||
"loading_encrypted": "端到端加密",
|
||||
"forms_switchHorizontalAxe": "切换到横向布局",
|
||||
"forms_switchVerticalAxe": "切换到纵向布局",
|
||||
"password_note1": "您的密码是用于加密所有文档的密钥。",
|
||||
"password_note2": "如果您丢失了密码,我们将无法恢复您的数据。",
|
||||
"password_note3": "您的密码是加密此实例上所有文档和管理员权限的密钥。",
|
||||
"computer_note": "如果您使用的是共享计算机,{0}请记住在操作完成后登出{1}。仅关闭浏览器窗口会导致账号暴露。",
|
||||
"import_note": "要保留您在未登录的情况下创建和/或存储的文档,请勾选“从访客会话导入文档”。",
|
||||
"admin_creation_note": "在此页面创建首个管理员账号。管理员可管理实例设置,包括存储配额,并有权使用审核工具。",
|
||||
"form_poll_required_hint": "此投票为必选项,您必须至少选择一个选项。",
|
||||
"fo_trash_move_error": "共享文件夹内容无法移至回收站,请改用“移除”。",
|
||||
"fo_sharedFolder_move_error": "共享文件夹无法移至其他共享文件夹内。"
|
||||
}
|
||||
|
||||
2
www/common/worker.bundle.min.js
vendored
2
www/common/worker.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@ -126,6 +126,7 @@
|
||||
|
||||
var start = function () {
|
||||
//config.document.key = key;
|
||||
const autosave = typeof(config.autosave) === "number" ? config.autosave : 10;
|
||||
chan.send('START', {
|
||||
key: key,
|
||||
application: config.documentType,
|
||||
@ -134,7 +135,7 @@
|
||||
documentKey: docID,
|
||||
document: blob,
|
||||
ext: config.document.fileType,
|
||||
autosave: config.events.onSave && (config.autosave || 10),
|
||||
autosave: config.events.onSave && autosave,
|
||||
readOnly: config.mode === 'view',
|
||||
editorConfig: config.editorConfig || {},
|
||||
_config: serializedConfig()
|
||||
@ -216,6 +217,12 @@
|
||||
});
|
||||
});
|
||||
|
||||
chan.on('DOCUMENT_ERROR', function (err) {
|
||||
if (config.events.onError) {
|
||||
config.events.onError(err);
|
||||
}
|
||||
});
|
||||
|
||||
chan.on('ON_DOWNLOADAS', blob => {
|
||||
let url = URL.createObjectURL(blob);
|
||||
if (!config.events.onDownloadAs) { return; }
|
||||
@ -232,6 +239,11 @@
|
||||
if (!config.events.onSave) { return void cb(); }
|
||||
config.events.onSave(data, cb);
|
||||
});
|
||||
chan.on('GET_BLOB', (data, cb) => {
|
||||
getBlob((err, blob) => {
|
||||
cb({error: err, blob});
|
||||
});
|
||||
});
|
||||
chan.on('RELOAD', function () {
|
||||
config.document.blob = blob;
|
||||
if (!config.editorConfig) { // Not OnlyOffice shim
|
||||
@ -378,6 +390,15 @@
|
||||
|
||||
chan.send('DOWNLOAD_AS', arg);
|
||||
};
|
||||
ret.save = () => {
|
||||
if (!chan) {
|
||||
return void onDocumentReady.push(() => {
|
||||
ret.save();
|
||||
});
|
||||
}
|
||||
|
||||
chan.send('MANUAL_SAVE');
|
||||
};
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
@ -86,7 +86,6 @@ define([
|
||||
var $mt = $dlview.find('media-tag');
|
||||
$mt.attr('src', src);
|
||||
$mt.attr('data-crypto-key', 'cryptpad:'+cryptKey);
|
||||
$mt.css('transform', 'scale(2)');
|
||||
|
||||
var rightsideDisplayed = false;
|
||||
var metadataReceived = false;
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
font-size: 1rem;
|
||||
&.alert-info {
|
||||
color: @cryptpad_text_col !important;
|
||||
margin-top: 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -749,13 +750,24 @@
|
||||
padding: 5px;
|
||||
margin-top: -10px;
|
||||
margin-right: -10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
svg.lucide {
|
||||
margin: 0;
|
||||
}
|
||||
&.cp-is-empty {
|
||||
height: fit-content;
|
||||
padding: 3px;
|
||||
border-radius: 0 @variables_radius 0 0;
|
||||
background-color: @cp_alerts-danger-bg;
|
||||
color: @cp_alerts-danger-text;
|
||||
}
|
||||
.cp-form-poll-required-info {
|
||||
opacity: 0.7;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -954,10 +966,10 @@
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
border-color: @cryptpad_text_col;
|
||||
border-right: 0px;
|
||||
}
|
||||
button {
|
||||
svg { margin: 0 !important; }
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2643,6 +2643,7 @@ define([
|
||||
poll: {
|
||||
defaultOpts: {
|
||||
type: 'text', // Text or Days or Time
|
||||
required: false,
|
||||
values: [1, 2, 3].map(function (i) {
|
||||
return {
|
||||
uid: Util.uid(),
|
||||
@ -2659,6 +2660,7 @@ define([
|
||||
var lines = makePollTable(answers, opts, false);
|
||||
|
||||
var disabled = false;
|
||||
var hasInteracted = false;
|
||||
|
||||
var updateAriaLabel = function ($c, val, index) {
|
||||
const optionLabel = $c.closest('.cp-form-type-poll').find(`.cp-form-poll-option[data-index=${index}] span`).text().trim();
|
||||
@ -2681,6 +2683,7 @@ define([
|
||||
|
||||
Util.onClickEnter($c, function () {
|
||||
if (disabled) { return; }
|
||||
hasInteracted = true;
|
||||
val = (val+1)%3;
|
||||
$c.attr('data-value', val);
|
||||
updateAriaLabel($c, val, index);
|
||||
@ -2717,10 +2720,24 @@ define([
|
||||
]);
|
||||
var $tag = $(tag);
|
||||
|
||||
if (APP.isEditor) {
|
||||
$tag[0].addEventListener('click', function (e) {
|
||||
const target = e.target;
|
||||
if (target.closest('.cp-form-required-radio')) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
var cursorGetter;
|
||||
var setCursorGetter = function (f) { cursorGetter = f; };
|
||||
return {
|
||||
tag: tag,
|
||||
isEmpty: function () {
|
||||
var v = this.getValue();
|
||||
if (!v || !v.values) { return true; }
|
||||
// Poll is considered empty if user hasn't interacted with it
|
||||
return !hasInteracted;
|
||||
},
|
||||
getValue: function () {
|
||||
var res = {};
|
||||
$tag.find('.cp-form-poll-choice').each(function (i, el) {
|
||||
@ -2733,6 +2750,7 @@ define([
|
||||
},
|
||||
reset: function () {
|
||||
$tag.find('.cp-form-poll-choice').attr('data-value', 0);
|
||||
hasInteracted = false;
|
||||
},
|
||||
setEditable: function (state) {
|
||||
disabled = !state;
|
||||
@ -2747,6 +2765,8 @@ define([
|
||||
this.reset();
|
||||
if (!res || !res.values) { return; }
|
||||
var val = res.values;
|
||||
// If there are saved values, user has interacted
|
||||
hasInteracted = Object.keys(val).length > 0;
|
||||
$tag.find('.cp-form-poll-choice').each(function (i, el) {
|
||||
if (!el._setValue) { return; }
|
||||
var $el = $(el);
|
||||
@ -4048,7 +4068,15 @@ define([
|
||||
|
||||
var requiredTag;
|
||||
if (block.opts && block.opts.required) {
|
||||
requiredTag = h('span.cp-form-required-tag', Messages.form_required_on);
|
||||
var requiredContent = [Messages.form_required_on];
|
||||
if (type === 'poll') {
|
||||
var infoIcon = Icons.get('help', {
|
||||
class: 'cp-form-poll-required-info',
|
||||
title: Messages.form_poll_required_hint
|
||||
});
|
||||
requiredContent.push(infoIcon);
|
||||
}
|
||||
requiredTag = h('span.cp-form-required-tag', requiredContent);
|
||||
}
|
||||
|
||||
var dragHandle;
|
||||
|
||||
@ -9,7 +9,8 @@ define([
|
||||
'/common/dom-ready.js',
|
||||
'/common/common-hash.js',
|
||||
'/common/sframe-common-outer.js',
|
||||
], function (nThen, ApiConfig, DomReady, Hash, SFCommonO) {
|
||||
'/common/outer/local-store.js'
|
||||
], function (nThen, ApiConfig, DomReady, Hash, SFCommonO, LocalStore) {
|
||||
|
||||
var href, hash;
|
||||
// Loaded in load #2
|
||||
@ -68,7 +69,7 @@ define([
|
||||
addData: addData,
|
||||
addRpc: addRpc,
|
||||
//cache: true,
|
||||
noDrive: parsed?.hashData?.mode !== "view",
|
||||
noDrive: (parsed?.hashData?.mode !== "view" || !LocalStore.isLoggedIn()),
|
||||
hash: hash,
|
||||
href: href,
|
||||
useCreationScreen: true,
|
||||
|
||||
@ -138,6 +138,9 @@ define([
|
||||
var onInsertImage = function (data, cb) {
|
||||
chan.send('ON_INSERT_IMAGE', data, cb);
|
||||
};
|
||||
var onError = function (err) {
|
||||
chan.send('DOCUMENT_ERROR', err);
|
||||
};
|
||||
var onReady = function () {
|
||||
chan.send('DOCUMENT_READY', {});
|
||||
};
|
||||
@ -157,10 +160,28 @@ define([
|
||||
chan.send('ON_DOWNLOADAS', blob);
|
||||
};
|
||||
|
||||
let manualSave;
|
||||
chan.on('MANUAL_SAVE', function () {
|
||||
if (typeof(manualSave) !== "function") {
|
||||
console.error('UNSUPPORTED COMMAND', 'save');
|
||||
return;
|
||||
}
|
||||
manualSave();
|
||||
});
|
||||
let setSave = f => {
|
||||
manualSave = f;
|
||||
};
|
||||
|
||||
|
||||
let getInstanceURL = function () {
|
||||
return Config.httpUnsafeOrigin;
|
||||
};
|
||||
let getBlobClient = (cb) => {
|
||||
chan.send('GET_BLOB', obj => {
|
||||
if (obj?.error) { console.error(obj?.error); }
|
||||
cb(obj?.blob);
|
||||
});
|
||||
};
|
||||
let getBlobServer = function (documentURL, cb) {
|
||||
let xhr = new XMLHttpRequest();
|
||||
let data = encodeURIComponent(documentURL);
|
||||
@ -174,11 +195,11 @@ define([
|
||||
// myBlob is now the blob that the object URL pointed to.
|
||||
cb(null, blob);
|
||||
} else {
|
||||
cb(this.status);
|
||||
getBlobClient(cb);
|
||||
}
|
||||
};
|
||||
xhr.onerror = function (e) {
|
||||
cb(e.message);
|
||||
xhr.onerror = function () {
|
||||
getBlobClient(cb);
|
||||
};
|
||||
xhr.send();
|
||||
};
|
||||
@ -248,9 +269,11 @@ define([
|
||||
_: data._config
|
||||
},
|
||||
utils: {
|
||||
onError,
|
||||
onReady: onReady,
|
||||
onDownloadAs,
|
||||
setDownloadAs,
|
||||
setSave,
|
||||
save: save,
|
||||
reload: reload,
|
||||
onHasUnsavedChanges: onHasUnsavedChanges,
|
||||
|
||||
@ -1052,6 +1052,19 @@ define([
|
||||
boards: boards,
|
||||
_boards: Util.clone(boards),
|
||||
});
|
||||
// disable dragging when editing
|
||||
$(document).on('mousedown', '.kanban-item input, .kanban-title-board input', function (e) {
|
||||
kanban.options.dragItems = false;
|
||||
e.stopPropagation();
|
||||
});
|
||||
$(document).on('mouseup', function (e) {
|
||||
var selectionActive = window.getSelection && window.getSelection().toString().length > 0;
|
||||
if (!selectionActive) {
|
||||
var drag = kanban.drag;
|
||||
kanban.options.dragItems = drag;
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
framework._.cpNfInner.metadataMgr.onChange(function () {
|
||||
var md = framework._.cpNfInner.metadataMgr.getPrivateData();
|
||||
|
||||
@ -78,6 +78,9 @@
|
||||
height: 5rem;
|
||||
min-width: 5rem;
|
||||
flex: 1;
|
||||
&:focus-visible {
|
||||
outline: @variables_focus_style;
|
||||
}
|
||||
}
|
||||
.cp-moderation-userdata-inputs {
|
||||
display: flex;
|
||||
@ -88,6 +91,9 @@
|
||||
:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
input:focus-visible{
|
||||
outline: @variables_focus_style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -421,7 +421,7 @@ define([
|
||||
// Msg.support_privacyHint.support_privacyTitle
|
||||
sidebar.addCheckboxItem({
|
||||
key: 'privacy',
|
||||
getState: () => false,
|
||||
getState: () => true,
|
||||
query: (val, setState) => {
|
||||
APP.support.setAnonymous(val);
|
||||
setState(val);
|
||||
@ -949,6 +949,7 @@ Attachments:${JSON.stringify(msg.attachments, 0, 2)}`;
|
||||
$container: APP.$toolbar,
|
||||
pageTitle: Messages.moderationPage,
|
||||
metadataMgr: common.getMetadataMgr(),
|
||||
skipLink: '#cp-sidebarlayout-container'
|
||||
};
|
||||
APP.toolbar = Toolbar.create(configTb);
|
||||
APP.toolbar.$rightside.hide();
|
||||
|
||||
@ -1083,6 +1083,7 @@ define([
|
||||
|
||||
framework.onReady(function(newPad) {
|
||||
editor.focus();
|
||||
editor.resetUndo();
|
||||
|
||||
if (!module.isMaximized) {
|
||||
module.isMaximized = true;
|
||||
|
||||
@ -140,6 +140,7 @@
|
||||
.cp-app-profile-displayname {
|
||||
font-weight: bold;
|
||||
font-size: 30px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.cp-app-profile-link {
|
||||
font-size: 25px;
|
||||
@ -185,7 +186,21 @@
|
||||
}
|
||||
.cp-app-profile-friend-request {
|
||||
flex: 0;
|
||||
margin-top: 10px;
|
||||
white-space: normal;
|
||||
margin-bottom: 0.5rem;
|
||||
svg {
|
||||
min-width: @variables_icon_width;
|
||||
}
|
||||
}
|
||||
.cp-app-profile-friend-request.btn-success {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
#cp-sidebarlayout-container #cp-sidebarlayout-rightside .cp-app-profile-friend-container button.btn.cp-app-profile-friend-request {
|
||||
height: auto;
|
||||
> span {
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
// I tried using flexbox but messed with how the pencil icon was displayed
|
||||
|
||||
@ -236,7 +236,7 @@ define([
|
||||
// If this curve has sent us a friend request, we should not be able to sent it to them
|
||||
var friendRequests = common.getFriendRequests();
|
||||
if (friendRequests[data.curvePublic]) {
|
||||
$button.append(Messages._getKey('friendRequest_received', [name || Messages.anonymous]))
|
||||
$button.append(UI.setHTML(h('span'), Messages._getKey('friendRequest_received', [name || Messages.anonymous])))
|
||||
.click(function () {
|
||||
UIElements.displayFriendRequestModal(common, friendRequests[data.curvePublic]);
|
||||
});
|
||||
@ -276,7 +276,7 @@ define([
|
||||
return;
|
||||
}
|
||||
// This is not a friend yet: we can send a friend request
|
||||
$button.text(Messages._getKey('userlist_addAsFriendTitle', [data.name || Messages.anonymous]))
|
||||
$button.empty().append([Icons.get('add-friend'), h('span', Messages._getKey('userlist_addAsFriendTitle', [data.name || Messages.anonymous]))])
|
||||
.click(function () {
|
||||
APP.common.sendFriendRequest({
|
||||
curvePublic: data.curvePublic,
|
||||
|
||||
@ -2031,6 +2031,9 @@ define([
|
||||
cat.forEach(function(c) {
|
||||
APP.$rightside.find('.' + c).show();
|
||||
});
|
||||
if (APP.editor) {
|
||||
APP.editor.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
var SIDEBAR_ICONS = {
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
color: @cryptpad_color_link;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.alert-info {
|
||||
.alert-info, .alert-warning {
|
||||
font-size: 16px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
@ -50,6 +50,7 @@ define([
|
||||
'cp-support-list',
|
||||
],
|
||||
'new': [ // Msg.support_cat_new
|
||||
'cp-support-custom',
|
||||
'cp-support-subscribe',
|
||||
'cp-support-language',
|
||||
'cp-support-form',
|
||||
@ -220,6 +221,18 @@ define([
|
||||
return $(content);
|
||||
};
|
||||
|
||||
create['custom'] = function () {
|
||||
const msg = AppConfig.customSupportMsg;
|
||||
if (!msg) { return $(); }
|
||||
const lang = Messages._getLanguage();
|
||||
const text = msg[lang] || msg['default'];
|
||||
if (!text) { return $(); }
|
||||
const div = h('div.cp-support-subscribe.cp-sidebarlayout-element', [
|
||||
h('div.alert.alert-warning', text)
|
||||
]);
|
||||
return $(div);
|
||||
};
|
||||
|
||||
// Create a new tickets
|
||||
create['form'] = function () {
|
||||
var key = 'form';
|
||||
@ -293,7 +306,8 @@ define([
|
||||
Object.keys(categories).forEach(function (key) {
|
||||
var $category = $('<div>', {
|
||||
'class': 'cp-sidebarlayout-category',
|
||||
'data-category': key
|
||||
'data-category': key,
|
||||
'tabindex': 0
|
||||
}).appendTo($categories);
|
||||
var iconClass = icons[key];
|
||||
if (iconClass) {
|
||||
@ -303,8 +317,7 @@ define([
|
||||
if (key === active) {
|
||||
$category.addClass('cp-leftside-active');
|
||||
}
|
||||
|
||||
$category.click(function () {
|
||||
Util.onClickEnter($category, function () {
|
||||
if (!Array.isArray(categories[key]) && categories[key].onClick) {
|
||||
categories[key].onClick();
|
||||
return;
|
||||
@ -329,6 +342,7 @@ define([
|
||||
$container: APP.$toolbar,
|
||||
pageTitle: Messages.supportPage,
|
||||
metadataMgr: common.getMetadataMgr(),
|
||||
skipLink: '#cp-sidebarlayout-container'
|
||||
};
|
||||
APP.toolbar = Toolbar.create(configTb);
|
||||
APP.toolbar.$rightside.hide();
|
||||
|
||||
@ -387,8 +387,8 @@ define([
|
||||
var privateData = metadataMgr.getPrivateData();
|
||||
|
||||
var answer = h('button.btn.btn-primary.cp-support-answer', [Icons.get('reply'), Messages.support_answer]);
|
||||
var close = h('button.btn.btn-danger.cp-support-close', [Icons.get('close'), Messages.support_close]);
|
||||
var remove = h('button.btn.btn-danger.cp-support-hide', [Icons.get('close'), Messages.support_remove]);
|
||||
var close = h('button.btn.btn-danger.cp-support-close', [Messages.support_close]);
|
||||
var remove = h('button.btn.btn-danger.cp-support-hide', [Icons.get('trash-full'), Messages.support_remove]);
|
||||
var _actions = [answer, close];
|
||||
if (content.closed && !ctx.isAdmin) {
|
||||
_actions = [remove]; // XXX update key to "Delete permanently" ?
|
||||
@ -454,7 +454,7 @@ define([
|
||||
if (onMove && !onMove.disableMove) {
|
||||
let text = onMove.isTicketActive ? Messages.support_movePending
|
||||
: Messages.support_moveActive;
|
||||
move = h('button.btn.btn-secondary', { title: text }, Icons.get('history'));
|
||||
move = h('button.btn.btn-secondary', { title: text }, Icons.get('archive'));
|
||||
Util.onClickEnter($(move), function () {
|
||||
onMove(ticket, id, content);
|
||||
});
|
||||
@ -481,7 +481,10 @@ define([
|
||||
let input = UI.dialog.textInput({id: `cp-${Util.uid()}`});
|
||||
$tags.append(input);
|
||||
let existing = onTag.getAllTags();
|
||||
let _field = UI.tokenField(input, existing).preventDuplicates(function (val) {
|
||||
let close = h('button.btn.btn-secondary.cp-token-close', [
|
||||
h('span', Messages.filePicker_close)
|
||||
]);
|
||||
let _field = UI.tokenField(input, existing, close).preventDuplicates(function (val) {
|
||||
UI.warn(Messages._getKey('tags_duplicate', [val]));
|
||||
});
|
||||
_field.setTokens(content.tags || []);
|
||||
@ -508,17 +511,10 @@ define([
|
||||
$list.toggle();
|
||||
$tags.toggle();
|
||||
});
|
||||
let close = h('button.btn.btn-secondary.cp-token-close', [
|
||||
Icons.get('close'),
|
||||
h('span', Messages.filePicker_close)
|
||||
]);
|
||||
Util.onClickEnter($(close), () => {
|
||||
$list.toggle(true);
|
||||
$tags.toggle(false);
|
||||
});
|
||||
setTimeout(() => {
|
||||
$tags.find('.cp-tokenfield-form').append(close);
|
||||
});
|
||||
}
|
||||
|
||||
adminActions = h('span.cp-support-title-buttons', [ url, move, tag, show ]);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user