Merge branch 'staging' into notification-page

This commit is contained in:
yflory 2025-06-27 15:36:38 +02:00
commit 878be22a56
103 changed files with 4290 additions and 2395 deletions

3
.gitignore vendored
View File

@ -35,6 +35,9 @@ www/common/onlyoffice/v*
www/common/worker.bundle.js
_build
FUNDING.json
funding.json
# ---> Node
# Logs
logs

View File

@ -4,11 +4,58 @@ SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and cont
SPDX-License-Identifier: AGPL-3.0-or-later
-->
# Release (2025.3.1)
# 2025.3.1
## Goals
This release introduces version 8 of OnlyOffice applications (Document, Sheet, Presentation) alongside smaller fixes and improvements.
## Features
- Upgrade OnlyOffice to version 8.3 [#1807](https://github.com/cryptpad/cryptpad/pull/1807)
## Improvements
- Show error message when x2t conversion fails in OO editor [#1890](https://github.com/cryptpad/cryptpad/pull/1890)
- Add x2t warning [#1887](https://github.com/cryptpad/cryptpad/pull/1887)
## Fixes
- Fix OnlyOffice document copy [#1864](https://github.com/cryptpad/cryptpad/pull/1864)
- Fix u8 increment issue in file-crypto [#1860](https://github.com/cryptpad/cryptpad/pull/1860) thanks @ansuz
- Fix eviction issues [#1856](https://github.com/cryptpad/cryptpad/pull/1856)
## Dependencies
- Upgrade TweetNaCl [#1854](https://github.com/cryptpad/cryptpad/pull/1854)
## Upgrade notes
> [!NOTE]
> The new version of OnlyOffice is much bigger than the previous versions. When installing version 8.3 with `install-onlyoffice.sh` ~830MB additional disk space is needed.
If you are upgrading from a version older than `2025.3.0` please read the upgrade notes of all versions between yours and `2025.3.1` to avoid configuration issues.
To upgrade:
1. Stop your server
2. Get the latest code with git
```bash
git fetch --depth 1 origin tag 2025.3.1
git checkout 2025.3.1
npm ci
npm run install:components
```
1. Restart your server
2. Review your instance's checkup page to ensure that you are passing all tests
## Contributors
Alumni: @ansuz
CryptPad team: @AAAMON @wginolas @yflory
# 💐 Spring 2025 release (2025.3.0)
## Goals

View File

@ -18,7 +18,7 @@ Note that every GitHub release page has an RSS compatible feed that you can subs
We do also communicate about this topic on:
- [Our blog](https://blog.cryptpad.org)
- [Our Matrix public space](https://matrix.to/#/#cryptpad:matrix.xwiki.com)
- [Our Mastodon account](https://fosstodon.org/@cryptpad)
- [Our Mastodon account](https://social.xwiki.com/@CryptPad)
## Reporting a Vulnerability

View File

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<head>
<title data-localization="main_title">CryptPad: Collaboration suite, encrypted and open-source</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 -256 1850 1850"
id="svg3013"
version="1.1"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
width="100%"
height="100%"
sodipodi:docname="forum.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata3023">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3021" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1403"
id="namedview3019"
showgrid="false"
inkscape:zoom="0.26081081"
inkscape:cx="-486.943"
inkscape:cy="498.4456"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg3013"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<g
transform="matrix(1,0,0,-1,37.966102,1305.4576)"
id="g3015"
style="fill:#0087ff;fill-opacity:1">
<path
d="m 1664,896 q 53,0 90.5,-37.5 Q 1792,821 1792,768 1792,715 1754.5,677.5 1717,640 1664,640 V 256 q 0,-52 -38,-90 -38,-38 -90,-38 -417,347 -812,380 -58,-19 -91,-66 -33,-47 -31,-100.5 2,-53.5 40,-92.5 -20,-33 -23,-65.5 -3,-32.5 6,-58 9,-25.5 33.5,-55 Q 683,41 706.5,20.5 730,0 768,-30 739,-88 656.5,-113 574,-138 488,-124.5 402,-111 356,-69 349,-46 326.5,18.5 304,83 294.5,113 q -9.5,30 -23,89 -13.5,59 -15,101 -1.5,42 3.5,98.5 5,56.5 22,110.5 H 160 Q 94,512 47,559 0,606 0,672 v 192 q 0,66 47,113 47,47 113,47 h 480 q 435,0 896,384 52,0 90,-38 38,-38 38,-90 V 896 z M 1536,292 v 954 Q 1142,944 768,903 V 633 q 377,-42 768,-341 z"
id="path3017"
inkscape:connector-curvature="0"
style="fill:#0087ff;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: Font Awesome by Dave Gandy
SPDX-License-Identifier: CC-BY-SA-3.0

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);}
</style>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="4.7893" y1="12.0669" x2="6.7712" y2="12.0669">
<stop offset="0" style="stop-color:#4592C4"/>
<stop offset="1" style="stop-color:#545ACD"/>
</linearGradient>
<polygon class="st0" points="4.8,13 6.4,13 6.8,11.2 5.1,11.2 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="0.4529" y1="12" x2="23.5471" y2="12">
<stop offset="0" style="stop-color:#4592C4"/>
<stop offset="1" style="stop-color:#545ACD"/>
</linearGradient>
<path class="st1" d="M21.6,17.8c0-0.1,0.1-0.1,0.1-0.2c1.2-1.7,1.8-3.8,1.8-6c0-6.2-5.2-11.2-11.5-11.2S0.5,5.4,0.5,11.6
c0,6.2,5.2,11.1,11.6,11.1c1.6,0,3.1-0.3,4.5-0.9c0,0,0.1,0,0.1,0c0,0,0,0,0,0c0.2-0.1,0.4-0.1,0.6-0.1c0.2,0,0.5,0,0.7,0.1
l4.7,1.7l-1.2-4.9C21.4,18.3,21.4,18.1,21.6,17.8z M9.1,11.2H7.5L7.2,13h1.5v0.7H7L6.6,16H5.8l0.5-2.3H4.7L4.2,16H3.5l0.4-2.3H2.5
V13h1.5l0.4-1.8H2.9v-0.7h1.6L5,8.1h0.7l-0.4,2.3h1.6l0.5-2.3h0.7l-0.5,2.3h1.4V11.2z M11.2,16h-0.9v-5.9h0.9V16z M11.1,8.9
C11,9,10.9,9.1,10.7,9.1c-0.2,0-0.3,0-0.4-0.1c-0.1-0.1-0.2-0.2-0.2-0.4c0-0.2,0.1-0.4,0.2-0.4c0.1-0.1,0.2-0.1,0.4-0.1
c0.1,0,0.3,0,0.4,0.1s0.2,0.2,0.2,0.4S11.2,8.9,11.1,8.9z M16.3,10.9c-0.2-0.1-0.5-0.1-0.6-0.1c-0.5,0-0.9,0.2-1.2,0.6
c-0.3,0.4-0.5,0.9-0.5,1.4V16h-0.9v-5.9h0.7l0.1,1.1h0c0.2-0.4,0.5-0.7,0.8-0.9c0.3-0.2,0.6-0.3,1-0.3c0.3,0,0.5,0,0.7,0.1
L16.3,10.9z M17.9,15.3c-0.5-0.5-0.7-1.3-0.7-2.2c0-1,0.2-1.7,0.7-2.3s1.2-0.8,2-0.8c0.3,0,0.6,0,0.8,0.1c0.3,0.1,0.5,0.1,0.7,0.2
l-0.3,0.8c-0.2-0.1-0.4-0.1-0.6-0.2c-0.2-0.1-0.4-0.1-0.6-0.1c-1.2,0-1.8,0.8-1.8,2.3c0,0.7,0.1,1.3,0.4,1.7s0.7,0.6,1.3,0.6
c0.5,0,1-0.1,1.5-0.3v0.8c-0.4,0.2-0.9,0.3-1.5,0.3C19.1,16.1,18.4,15.8,17.9,15.3z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,3 +0,0 @@
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
SPDX-License-Identifier: AGPL-3.0-or-later

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#Shape_2_);}
</style>
<g id="Octicons_1_">
<g id="issue-opened">
<linearGradient id="Shape_2_" gradientUnits="userSpaceOnUse" x1="1.1619" y1="12" x2="22.8381" y2="12">
<stop offset="0" style="stop-color:#4592C4"/>
<stop offset="1" style="stop-color:#545ACD"/>
</linearGradient>
<path id="Shape_1_" class="st0" d="M12,3.2c4.9,0,8.8,4,8.8,8.8s-4,8.8-8.8,8.8s-8.8-4-8.8-8.8S7.1,3.2,12,3.2L12,3.2z M12,1.2
C6,1.2,1.2,6,1.2,12S6,22.8,12,22.8S22.8,18,22.8,12S18,1.2,12,1.2L12,1.2z M13.5,5.8h-3.1v7.7h3.1V5.8L13.5,5.8z M13.5,15.1h-3.1
v3.1h3.1V15.1L13.5,15.1z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 979 B

View File

@ -1,3 +0,0 @@
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
SPDX-License-Identifier: AGPL-3.0-or-later

View File

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0.1 (1.0.1+r73)"
sodipodi:docname="twiitter.svg"
xml:space="preserve"
style="enable-background:new 0 0 24 24;"
viewBox="0 0 24 24"
y="0px"
x="0px"
id="Layer_1"
version="1.1"><metadata
id="metadata1416"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs1414" /><sodipodi:namedview
inkscape:current-layer="Layer_1"
inkscape:window-maximized="0"
inkscape:window-y="10"
inkscape:window-x="10"
inkscape:cy="14.107333"
inkscape:cx="-5.7258526"
inkscape:zoom="17.645834"
showgrid="false"
id="namedview1412"
inkscape:window-height="1036"
inkscape:window-width="1900"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:document-rotation="0" />
<style
id="style1402"
type="text/css">
.st0{fill:url(#SVGID_1_);}
</style>
<linearGradient
y2="12"
x2="23.6009"
y1="12"
x1="0.3991"
gradientUnits="userSpaceOnUse"
id="SVGID_1_">
<stop
id="stop1404"
style="stop-color:#4592C4"
offset="0" />
<stop
id="stop1406"
style="stop-color:#545ACD"
offset="1" />
</linearGradient>
<path
style="fill:#0087ff;fill-opacity:1"
id="path1409"
d="M23.6,4.8c-0.9,0.4-1.8,0.6-2.7,0.8C21.9,5,22.6,4,23,2.9c-0.9,0.5-1.9,0.9-3,1.2c-0.9-0.9-2.1-1.5-3.5-1.5 c-2.6,0-4.8,2.1-4.8,4.8c0,0.4,0,0.7,0.1,1.1c-4-0.2-7.5-2.1-9.8-5C1.6,4.1,1.4,5,1.4,5.8c0,1.7,0.8,3.1,2.1,4 C2.7,9.8,2,9.6,1.3,9.2c0,0,0,0,0,0.1c0,2.3,1.6,4.2,3.8,4.7c-0.4,0.1-0.8,0.2-1.3,0.2c-0.3,0-0.6,0-0.9-0.1 c0.6,1.9,2.4,3.3,4.4,3.3c-1.6,1.3-3.7,2-5.9,2c-0.4,0-0.8,0-1.1-0.1c2.1,1.4,4.6,2.2,7.3,2.2c8.8,0,13.6-7.3,13.6-13.6 c0-0.2,0-0.4,0-0.6C22.2,6.6,23,5.8,23.6,4.8z"
class="st0" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,3 +0,0 @@
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
SPDX-License-Identifier: AGPL-3.0-or-later

View File

@ -15,13 +15,13 @@ define([
elem.innerHTML = [
'<div class="cp-loading-logo">',
'<img class="cp-loading-cryptofist" src="/api/logo?' + urlArgs + '" alt="' + Messages.label_logo + '">',
'<img class="cp-loading-cryptofist" src="/api/logo?' + urlArgs + '" alt="" aria-hidden="true">',
'</div>',
'<div class="cp-loading-container">',
'<div class="cp-loading-spinner-container">',
'<span class="cp-spinner"></span>',
'</div>',
'<div class="cp-loading-progress">',
'<div class="cp-loading-progress" aria-hidden="true" role="presentation">',
'<div class="cp-loading-progress-list"></div>',
'<div class="cp-loading-progress-container"></div>',
'</div>',

View File

@ -104,9 +104,9 @@ define([
var proceed = function (result) {
hashing = false;
if (cb && typeof cb === "function" && cb(result)) { return; }
LocalStore.clearLoginToken();
Realtime.whenRealtimeSyncs(result.realtime, function () {
if (cb && typeof cb === "function" && cb(result)) { return; }
LocalStore.clearLoginToken();
Exports.redirect();
});
};

View File

@ -134,6 +134,7 @@ define(req, function(AppConfig, Default, Language) {
}
};
Messages.help_close_button = 'Close help notification'; // XXX
// XXX
Messages.badges_admin = "Instance administrator";
Messages.badges_moderator = "Instance moderator";
@ -144,8 +145,19 @@ define(req, function(AppConfig, Default, Language) {
Messages.add_password = "Add your document password";
Messages.show_password = "Show password";
Messages.hide_password = "Hide password";
Messages.page_next = "Next page";
Messages.page_previous = "Previous page";
Messages.contact_mastodon = 'Mastodon'; // XXX
Messages.contact_forum = 'Forum'; // XXX
Messages.toolbar_text_tools = "Text tools";
Messages.toolbar_show_text_tools = "Show text tools";
Messages.toolbar_hide_text_tools = "Hide text tools";
Messages.settings_profileAvatarLabel = "Avatar";
Messages.settings_profileHeaderTitle = "Edit your profile";
Messages.settings_profileHeaderHint = "Edit your public profile...";
Messages.next_templateList = "Next template list";
Messages.previous_templateList = "Previous template list";
Messages.fc_open_formaud = 'Open (as auditor)';
return Messages;
});

View File

@ -10,8 +10,10 @@ define([
'/customize/messages.js',
'jquery',
'/api/config',
'/common/extensions.js',
'optional!/api/instance',
], function (h, Language, Util, AppConfig, Msg, $, ApiConfig, Instance) {
], function (h, Language, Util, AppConfig, Msg, $, ApiConfig,
Extensions, Instance) {
var Pages = {};
Pages.setHTML = function (e, html) {
@ -49,15 +51,8 @@ define([
return Pages.externalLink(el, Pages.localizeDocsLink(href));
};
var accounts = Pages.accounts = {
Pages.accounts = {
donateURL: AppConfig.donateURL || "https://opencollective.com/cryptpad/",
upgradeURL: AppConfig.upgradeURL
};
Pages.areSubscriptionsAllowed = function () {
try {
return ApiConfig.allowSubscriptions && accounts.upgradeURL && !ApiConfig.restrictRegistration;
} catch (err) { return void console.error(err); }
};
var languageSelector = function () {
@ -82,6 +77,7 @@ define([
if (!ref) { return; }
var attrs = {
href: ref,
role: 'button',
};
var iconName = '';
if (!/^\//.test(ref)) {
@ -160,7 +156,7 @@ define([
return h('footer.cp-footer', [
h('div.cp-footer-left', [
h('a', {href:"https://cryptpad.org"}, [
h('a', {href:"https://cryptpad.org", role: 'button'}, [
h('div.cp-logo-foot', [
h('img', {
src: '/customize/CryptPad_logo.svg',
@ -193,7 +189,7 @@ define([
var registerLink;
if (!ApiConfig.restrictRegistration) {
registerLink = h('a.nav-item.nav-link.cp-register-btn', { href: '/register/'}, [
registerLink = h('a.nav-item.nav-link.cp-register-btn', { href: '/register/', role: 'button'}, [
h('i.fa.fa-user', {'aria-hidden':'true'}),
Msg.login_register
]);
@ -201,14 +197,14 @@ define([
if (username === null) {
rightLinks = [
h('a.nav-item.nav-link.cp-login-btn', { href: '/login/'}, [
h('a.nav-item.nav-link.cp-login-btn', { href: '/login/', role: 'button'}, [
h('i.fa.fa-sign-in', {'aria-hidden':'true'}),
Msg.login_login
]),
registerLink,
];
} else {
rightLinks = h('a.nav-item.nav-link.cp-user-btn', { href: '/drive/' }, [
rightLinks = h('a.nav-item.nav-link.cp-user-btn', { href: '/drive/', role: 'button'}, [
h('i.fa.fa-user-circle', {'aria-hidden':'true'}),
" ",
username
@ -216,7 +212,7 @@ define([
}
var isHome = ['/', '/index.html'].includes(window.location.pathname);
var homeLink = h('a.nav-item.nav-link.cp-back-home' /* .navbar-brand */, { href: '/index.html' }, [
var homeLink = h('a.nav-item.nav-link.cp-back-home' /* .navbar-brand */, { href: '/index.html', role: 'button'}, [
h('i.fa.fa-arrow-left'),
h('img', {
src: '/customize/CryptPad_logo.svg',
@ -226,12 +222,19 @@ define([
Msg.homePage
]);
let pricingName = Msg.features;
Extensions.getExtensionsSync('PRICING_NAME').some(ext => {
if (!ext.name) { return; }
pricingName = ext.name;
return true;
});
return h('nav.navbar.navbar-expand-lg',
[
!isHome? homeLink: undefined,
h('a.nav-item.nav-link', { href: '/features.html'}, [
h('a.nav-item.nav-link', { href: '/features.html', role: 'button'}, [
h('i.fa.fa-info-circle'),
Pages.areSubscriptionsAllowed()? Msg.pricing: Msg.features
pricingName
]),
h('a.nav-item.nav-link', { href: 'https://docs.cryptpad.org'},
[h('i.fa.fa-book', {'aria-hidden':'true'}),Msg.docs_link]),

View File

@ -19,10 +19,38 @@ define([
].indexOf(Config.adminEmail) === -1;
var adminMailbox = Config.supportMailbox && LocalStore.isLoggedIn();
let contacts = [
{
name: Msg.contact_chat || "Chat",
image: "/customize/images/sayhi.svg",
href: "https://matrix.to/#/#cryptpad:matrix.xwiki.com",
},
{
name: Msg.contact_mastodon || "Mastodon",
image: "/customize/images/mastodon.svg",
href: "https://social.xwiki.com/@CryptPad",
},
{
name: Msg.contact_bug || "Bug report",
image: "/customize/images/github.svg",
href: "https://github.com/cryptpad/cryptpad/issues/",
},
{
name: Msg.contact_forum || "Forum",
image: "/customize/images/forum.svg",
href: "https://forum.cryptpad.org/",
},
{
name: Msg.contact_email || "Email",
image: "/customize/images/email.svg",
href: 'mailto:' + developerEmail,
},
];
return h('div#cp-main', [
Pages.infopageTopbar(),
h('div.container.cp-container', [
h('div.row.cp-page-title',[
h('div.row.cp-page-title', [
h('div.col-12.text-center', h('h1', Msg.contact)),
]),
(adminEmail || adminMailbox) ? h('div.row.cp-iconCont.align-items-center', [
@ -64,76 +92,22 @@ define([
Pages.setHTML(h('h2.text-center'), Msg.contact_dev),
h('p.center', Msg.contact_devHint)
),
].concat(contacts.map(item =>
h('div',
h('a.card-small', {href : "https://matrix.to/#/#cryptpad:matrix.xwiki.com"},
h('a.card-small', { href: item.href },
h('div.card-body',
h('p', [
h('img', {
src: '/customize/images/sayhi.svg',
src: item.image,
alt: '',
'aria-hidden': 'true'
}),
Msg.contact_chat || 'Chat'
item.name
])
)
)
),
h('div',
h('a.card-small', {href : "https://fosstodon.org/@cryptpad"},
h('div.card-body',
h('p', [
h('img', {
src: '/customize/images/mastodon.svg',
alt: '',
'aria-hidden': 'true'
}),
'Mastodon'
])
)
)
),
h('div',
h('a.card-small', {href : "https://twitter.com/cryptpad"},
h('div.card-body',
h('p', [
// this is not a typo. adblock plus blocks images with src *twitter* apparently
h('img', {
src: '/customize/images/twiitter.svg',
alt: '',
'aria-hidden': 'true'}),
'Twitter'
])
)
)
),
h('div',
h('a.card-small', {href : "https://github.com/cryptpad/cryptpad/issues/"},
h('div.card-body',
h('p', [
h('img', {
src: '/customize/images/github.svg',
alt: '',
'aria-hidden': 'true'}),
Msg.contact_bug || 'Bug report'
])
)
)
),
h('div',
h('a.card-small', {href : "mailto:" + developerEmail},
h('div.card-body',
h('p', [
h('img', {
src: '/customize/images/email.svg',
alt: '',
'aria-hidden': 'true'
}),
Msg.contact_email || 'Email'
])
)
)
),
]),
)
))),
]),
Pages.infopageFooter(),
]);

View File

@ -13,9 +13,8 @@ define([
'/common/common-ui-elements.js',
'/common/common-constants.js',
'/common/pad-types.js',
], function ($, h, Msg, AppConfig, LocalStore, Pages, Config, UIElements, Constants, PadTypes) {
var accounts = Pages.accounts;
'/common/extensions.js'
], function ($, h, Msg, AppConfig, LocalStore, Pages, Config, UIElements, Constants, PadTypes, Extensions) {
return function () {
document.title = Msg.features;
Msg.features_f_apps_note = PadTypes.availableTypes.map(function (app) {
@ -25,12 +24,6 @@ define([
AppConfig.enableEarlyAccess) { return; }
return Msg.type[app];
}).filter(function (x) { return x; }).join(', ');
var premiumButton = h('a', {
href: accounts.upgradeURL,
target: '_blank',
rel: 'noopener noreferrer',
class: 'cp-features-register-button',
}, Msg.features_f_subscribe);
var groupItemTemplate = function (title, content) {
return h('li.list-group-item', [
@ -127,39 +120,24 @@ define([
]),
]),
]);
var premiumFeatures =
h('div.col-12.col-sm-4.cp-premium-user',[
h('div.card',[
h('div.title-card',[
h('h3.text-center',Msg.features_premium)
]),
h('div.card-body.cp-pricing',[
h('div.text-center', h('a', {
href: accounts.upgradeURL,
target: '_blank'
}, Msg._getKey('features_pricing', ['5', '10', '15']))),
h('div.text-center', Msg.features_emailRequired),
]),
h('ul.list-group.list-group-flush', [
'reg', // Msg.features_f_reg, .features_f_reg_note
'storage2',
'support', // Msg.features_f_support, .features_f_support_note
'supporter' // Msg.features_f_supporter, .features_f_supporter_note
].map(groupItem)),
h('div.card-body',[
h('div.cp-features-register#cp-features-subscribe', [
premiumButton
]),
LocalStore.isLoggedIn() ? undefined : h('div.cp-note', Msg.features_f_subscribe_note)
]),
]),
]);
var availableFeatures = [
anonymousFeatures,
registeredFeatures,
Pages.areSubscriptionsAllowed() ? premiumFeatures: undefined,
];
// Msg.features_premium
// Msg.features_pricing
// Msg.features_emailRequired
// Msg.features_f_subscribe, .features_f_subscribe_note
// Msg.features_f_reg, .features_f_reg_note
// Msg.features_f_support, .features_f_support_note
// Msg.features_f_supporter, .features_f_supporter_note
Extensions.getExtensionsSync('EXTRA_PRICING').forEach(ext => {
if (!ext.getContent) { return; }
availableFeatures.push(ext.getContent(groupItem));
});
return h('div#cp-main', [
Pages.infopageTopbar(),
h('div.container.cp-container',[

View File

@ -149,20 +149,15 @@ define([
locationBlock = h('div', h('br'));
}
var subButton = function () {
if (Pages.areSubscriptionsAllowed() && !LocalStore.getPremium()) {
var sub = h('div.cp-sub-prompt', [
h('span', Msg.home_morestorage),
h('a', {href:"/accounts/", class:'subscribe-btn'}, [
h('i.fa.fa-ticket'),
Msg.features_f_subscribe
])
]);
return sub;
} else {
return h('div');
}
};
let extraButtons = [];
// Messages.home_morestorage
// Messages.features_f_subscribe
Extensions.getExtensionsSync('HOMEPAGE_BUTTON').forEach(ext => {
if (!ext.getButton) { return; }
const b = ext.getButton();
if (!b) { return; }
extraButtons.push(b);
});
let popup = h('div.cp-extensions-popups');
@ -220,7 +215,7 @@ define([
h('i.fa.fa-hdd-o', {'aria-hidden': 'true'}),
Msg.team_cat_drive
]),
subButton
extraButtons
])
])
]),

View File

@ -12,7 +12,7 @@ var elem = document.createElement('div');
elem.setAttribute('id', 'placeholder');
elem.innerHTML = [
'<div class="placeholder-logo-container">',
'<img class="placeholder-logo" alt="CryptPad Logo" src="' + logoPath + '">',
'<img class="placeholder-logo" alt="" aria-hidden="true" src="' + logoPath + '">',
'</div>',
'<div class="placeholder-message-container">',
'<p>Loading...</p>',

View File

@ -7,6 +7,7 @@
@import (reference) "./tools.less";
@import (reference) "./colortheme-all.less";
@import (reference) "./variables.less";
@import (reference) "./badges.less";
.avatar_vars(
@width: 30px
) {
@ -22,15 +23,21 @@
.avatar_vars(@width);
--avatar-width: @avatar-width;
--avatar-font-size: @avatar-font-size;
@badge-width: min(0.4*@width, 100px);
--badges-width: @badge-width;
--badges-offset: ceil(-1*@badge-width/5);
--badges-font: ceil(@badge-width*2/3);
}
& {
.avatar_vars();
&.cp-avatar {
overflow: visible;
text-overflow: ellipsis;
font-size: 16px;
display: flex;
align-items: center;
border-radius: @variables_radius;
position: relative;
.cp-avatar-default, media-tag {
display: inline-flex;
@ -76,6 +83,14 @@
max-height: none; // To override 'media-tag img' in slide.less
}
}
.badges_include();
& > i.cp-badge {
position: absolute;
z-index: 10;
bottom: var(--badges-offset);
right: var(--badges-offset);
border: 1px solid @cp_badges-color;
}
}
}

View File

@ -17,18 +17,33 @@
--badges-width: @badges-width;
--badges-font: ceil(@badges-width*2/3);
}
.badges_include() {
--LessLoader_require: LessLoader_currentFile();
}
& {
.badges_vars();
i.cp-badge {
width: var(--badges-width);
height: var(--badges-width);
box-sizing: border-box;
border: 1px solid @cryptpad_text_col;
box-sizing: content-box;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: var(--badges-font);
color: @cp_badges-color;
background-color: @cryptpad_color_link;
z-index: 10;
&.cp-selected {
outline-offset: 3px;
outline: 2px solid @cryptpad_text_col;
}
each(@cp_badges-bg, {
&[data-badge="@{key}"] {
background-color: @value;
}
});
}
}

View File

@ -98,6 +98,14 @@
darken(desaturate(extract(@cp_palette, 7),60%), 60%),
darken(desaturate(extract(@cp_palette, 8),70%), 60%);
@cp_badges-color: @cp_app-bg;
@cp_badges-bg: {
admin: #99cfff;
moderator: #ffb8a6;
premium: #deb2ff;
error: @cryptpad_color_red,;
}
@cryptpad_color_link:@cryptpad_color_brand_300;
// Premium plans colors
@ -321,6 +329,8 @@
@cp_toolbar-bottom-bg: @cryptpad_text_col;
@cp_toolbar-bottom-fg: @cryptpad_color_grey_900;
@cp_toolbar-logo-bg: @cryptpad_color_grey_200;
@cp_toolbar-bg2: @cryptpad_color_grey_850;
@cp_toolbar-bg3: @cryptpad_color_grey_800;
@cp_toolbar-fade1: fade(@cryptpad_text_col, 10%);
@cp_toolbar-fade3: fade(@cryptpad_text_col, 30%);

View File

@ -97,6 +97,14 @@
darken(extract(@cp_palette, 7), 50%),
darken(extract(@cp_palette, 8), 50%);
@cp_badges-color: @cp_app-bg;
@cp_badges-bg: {
admin: #0087ff;
moderator: #ff6038;
premium: #b864f7;
error: @cryptpad_color_red;
}
@cryptpad_color_link: @cryptpad_color_brand;
// Premium plans colors
@ -320,6 +328,10 @@
@cp_toolbar-bottom-bg: @cryptpad_color_grey_200;
@cp_toolbar-bottom-fg: @cryptpad_text_col;
@cp_toolbar-logo-bg: @cryptpad_color_white;
//@cp_toolbar-bg2: #e5e5e5;
//@cp_toolbar-bg3: #b2b2b2;
@cp_toolbar-bg2: @cryptpad_color_grey_200;
@cp_toolbar-bg3: @cryptpad_color_grey_300;
@cp_toolbar-fade1: fade(@cryptpad_color_black, 10%);
@cp_toolbar-fade3: fade(@cryptpad_color_black, 30%);

View File

@ -129,6 +129,7 @@
}
a:focus-visible {
border-radius: 50%;
outline: @variables_focus_style;
}
}
}
@ -154,6 +155,10 @@
&:hover, &:focus-visible {
background: contrast(@cp_creation-button-fg, lighten(@cp_creation-button-bg, 5%), darken(@cp_creation-button-bg, 5%));
}
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
}
}
@ -268,10 +273,18 @@
width: 45px;
margin: 0 5px;
padding: 0 0 0 3px;
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
}
select {
border-radius: @variables_radius;
margin: 0.2rem;
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
}
}
&.active {
@ -293,12 +306,20 @@
width: 150px;
padding: 0 5px;
margin: 0.2rem;
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
}
label {
flex: none;
}
.cp-password-reveal {
margin:0.2rem;
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
}
}
}
@ -341,6 +362,10 @@
&.hidden {
visibility: hidden;
}
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
}
}
.cp-creation-template-container {
@ -379,6 +404,10 @@
color: var(--creation-color);
}
}
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
transition: all 0.1s;

View File

@ -138,6 +138,12 @@
display: none;
}
}
#cp-app-drive-content-container {
.cp-splitter {
display: none;
}
}
}
}
@ -493,7 +499,7 @@
}
}
}
/* CONTENT */
#cp-app-drive-content-container {
display: flex;
@ -512,6 +518,13 @@
display: flex;
align-items: center;
justify-content: flex-end;
touch-action: none;
-ms-touch-action: none;
-webkit-touch-action: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
&:hover {
border-left: 1px solid @cryptpad_text_col;
}

View File

@ -251,6 +251,21 @@
}
}
&.btn-case {
text-transform: capitalize;
font-size: 16px;
}
&.btn-primary-alt {
background-color: @cryptpad_color_brand_300;
border-color: @cryptpad_color_brand_300;
color: @cryptpad_color_grey_800;
&:hover, &:not(:disabled):active, &:focus {
color: @cryptpad_color_grey_800;
border-color: @cryptpad_color_brand_300;
background-color: lighten(@cryptpad_color_brand_300, 5%);
}
}
&.btn-light {
border-color: @cryptpad_text_col;
color: @cryptpad_text_col;

View File

@ -5,6 +5,7 @@
*/
@import (reference) "./colortheme-all.less";
@import (reference) "./variables.less";
.help_main () {
--LessLoader_require: LessLoader_currentFile();
@ -15,17 +16,21 @@
position: relative;
background-color: @cp_help-bg;
max-height: 50%;
overflow-y: auto;
&.cp-help-hidden {
display: none;
}
.cp-help-close {
background: none;
border: none;
position: absolute;
color: @cryptpad_text_col;
top: 5px;
right: 5px;
cursor: pointer;
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
}
.cp-help-text {
color: @cp_help-fg;
@ -34,6 +39,10 @@
a {
color: @cp_help-link;
text-decoration: underline;
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
}
h1 {
font-size: 20px;

View File

@ -110,6 +110,7 @@
overflow-y: auto;
a {
color: @cp_loading-link;
border-radius: @variables_radius;
}
p:last-child {
margin-bottom: 0;
@ -134,10 +135,17 @@
}
.cp-password-reveal {
margin-left: 0.5rem;
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
}
}
.cp-password-input {
font-size: 16px;
&:focus-visible {
outline: @variables_focus_style;
}
}
.tools_placeholder-color();
}

View File

@ -289,3 +289,32 @@
}
}
//reusable styling for the markdown toggle button across all apps
.markdown-toolbar-button-style() {
padding: 3px 5px;
height: fit-content;
margin: 0;
line-height: 1;
border-radius: @variables_radius;
display: inline-flex;
background-color: @cp_toolbar-bottom-bg;
color: @cp_toolbar-bottom-fg;
&:hover {
background-color: fade(@cryptpad_text_col, 20%);
border-color: @cryptpad_text_col;
color: @cryptpad_text_col;
}
&.cp-toolbar-button-active {
background-color: @cp_toolbar-bg;
border-color: @cp_toolbar-fg;
color: @cp_toolbar-fg;
&:hover {
background-color: fade(@cryptpad_text_col, 20%);
}
}
}

View File

@ -5,6 +5,7 @@
*/
@import (reference) './avatar.less';
@import (reference) './badges.less';
@import (reference) "./colortheme-all.less";
.messenger_vars (
@ -12,12 +13,12 @@
) {
@msg-color: @cryptpad_text_col;
@msg-color-hover: contrast(@msg-color, lighten(@msg-color, 10%), darken(@msg-color, 10%));
@msg-bg-color: @bg-color;
@invert: @bg-color;
@msg-bg-color-light: contrast(@invert, lighten(@bg-color, 5%), darken(@bg-color, 5%));
@msg-bg-color-lighter: contrast(@invert, lighten(@bg-color, 10%), darken(@bg-color, 10%));
@msg-bg-color-dark: fade(@cp_messenger-fg, 10%);
@msg-bg-color-darker: contrast(@bg-color, lighten(@bg-color, 10%), darken(@bg-color, 10%));
@msg-bg-color: @cp_app-bg;
@msg-bg-color-dark: @cp_toolbar-bg2;
@msg-bg-color-darker: @cp_toolbar-bg3;
@msg-bg-color-light: @cp_toolbar-bg2;
@msg-bg-color-lighter: @cp_toolbar-bg3;
};
.messenger_main(
@bg-color: @cp_messenger-bg,
@ -116,9 +117,17 @@
}
}
}
i.cp-badge {
color: var(--msg-bg-color-dark);
border-color: var(--msg-bg-color-dark);
}
&:hover {
background-color: @msg-bg-color-darker;
background-color: var(--msg-bg-color-darker);
i.cp-badge {
color: var(--msg-bg-color-darker);
border-color: var(--msg-bg-color-darker);
}
}
&.cp-app-contacts-notify {
animation: notif 2s ease-in-out infinite;
@ -163,7 +172,6 @@
}
.cp-app-contacts-muted-button {
margin: 10px;
border: 0;
display: none;
order: 3;
.fa-bell-slash {
@ -192,7 +200,7 @@
}
#cp-app-contacts-friendlist .cp-app-contacts-friend, #cp-app-contacts-messaging .cp-avatar {
.avatar_main(30px);
.avatar_main(35px);
&.cp-avatar {
display: flex;
}
@ -207,7 +215,6 @@
//margin-right: 5px;
flex-shrink: 0;
z-index: 1;
margin: 4px;
}
.cp-app-contacts-status {
display: inline-block;
@ -238,8 +245,12 @@
padding: 20px;
}
.cp-app-contacts-header {
background-color: @msg-bg-color-dark;
background-color: var(--msg-bg-color-dark);
background-color: @msg-bg-color-lighter;
background-color: var(--msg-bg-color-lighter);
i.cp-badge {
color: var(--msg-bg-color-lighter);
border-color: var(--msg-bg-color-lighter);
}
padding: 0;
display: flex;
justify-content: space-between;
@ -256,15 +267,20 @@
}
}
.cp-avatar,
.cp-avatar-container,
.cp-app-contacts-right-col {
flex: 1 1 auto;
text-align: center;
}
.cp-app-contacts-remove-history {
.hover;
}
.cp-avatar-container {
display: flex;
align-items: center;
}
.cp-avatar {
margin: 10px;
margin: 0 10px;
}
.cp-app-contacts-more-history {
//display: none;

View File

@ -355,4 +355,19 @@
color: @cp_form-invalid;
}
}
.cp-userteam-picker {
.cp-userteam-filter {
margin: 5px 0;
input {
margin-bottom: 0 !important;
}
}
.cp-usergrid-container {
label {
margin-bottom: 0;
}
}
}
}

View File

@ -64,7 +64,10 @@
}
.cp-markdown-toolbar {
height: @toolbar_line-height;
min-height: @toolbar_line-height;
padding: 0.1rem 0;
height: auto;
flex-wrap: wrap;
background-color: @cp_toolbar-bg;
color: @cp_toolbar-fg;
display: none;
@ -74,7 +77,6 @@
border-radius: @variables_radius;
color: @toolbar-color;
.toolbar_button;
font: normal normal normal 14px/1 FontAwesome;
&:hover {
background-color: contrast(@cp_toolbar-bg, darken(@cp_toolbar-bg, 5%), lighten(@cp_toolbar-bg, 5%));
}
@ -85,6 +87,11 @@
button:nth-of-type(1) {
margin-left: 0.3rem;
}
@media screen and (max-width: @browser_media-medium-screen) {
button:nth-of-type(1) {
margin-left: 0;
}
}
}
.cp-toolbar-container {
@ -108,7 +115,7 @@
border: 1px solid transparent;
color: inherit;
font: @toolbar_button-font;
* {
*:not(i) {
color: inherit;
font: @toolbar_button-font;
}
@ -152,8 +159,8 @@
}
.cp-toolbar-userlist-drawer {
font: @colortheme_app-font-size @colortheme_font;
min-width: 175px;
width: 175px;
min-width: 200px;
width: 200px;
display: block;
overflow-y: auto;
overflow-x: hidden;
@ -203,20 +210,30 @@
flex-flow: column;
margin: 10px 0;
margin-bottom: 20px;
.cp-userlist-entry {
display: flex;
align-items: center;
}
&>span {
height: 48px;
padding: 5px;
margin: 2px 0;
background: @cp_toolbar-fade1;
background: @cp_toolbar-bg2;
border-radius: @variables_radius;
border-right: 3px solid transparent;
.avatar_main(30px);
.cp-avatar-default, media-tag {
margin-right: 5px;
.avatar_main(35px);
i.cp-badge {
color: @cp_toolbar-bg2;
border-color: @cp_toolbar-bg2;
}
&.cp-userlist-clickable {
cursor: pointer;
&:hover {
background: @cp_toolbar-fade3;
background: @cp_toolbar-bg3;
i.cp-badge {
color: @cp_toolbar-bg3;
border-color: @cp_toolbar-bg3;
}
}
}
.cp-toolbar-userlist-rightcol {
@ -224,6 +241,7 @@
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 5px;
white-space: nowrap;
display: flex;
flex-flow: column;
@ -398,7 +416,6 @@
.fa {
font: normal normal normal 14px/1 FontAwesome;
font-family: FontAwesome;
}
.tools_unselectable();

View File

@ -51,6 +51,14 @@
margin-right: 0px !important;
}
}
&:not(.radio) {
.cp-usergrid-user {
transition: order 0.5s, background-color 0.5s;
&.cp-selected {
order: -1 !important;
}
}
}
.cp-usergrid-user {
width: 70px;
height: 70px;
@ -62,14 +70,12 @@
border-radius: @variables_radius;
margin: 3px !important;
cursor: default;
transition: order 0.5s, background-color 0.5s;
margin-top: 1px;
.tools_unselectable();
&.cp-selected {
background-color: @cp_usergrid-selected-bg;
color: @cp_usergrid-selected-fg;
order: -1 !important;
.cp-usergrid-avatar {
media-tag, .cp-avatar-default {
opacity: 0.7;

View File

@ -5,7 +5,7 @@
---
services:
cryptpad:
image: "cryptpad/cryptpad:version-2025.3.0"
image: "cryptpad/cryptpad:version-2025.6.0"
hostname: cryptpad
environment:

View File

@ -92,7 +92,7 @@ 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+3 6a00704df88c87bf5810257d99e83b32b76b86326ebd668b09c257f0abc283991b9ec3c121e0801173afc4b8deb21d4539c307da26e40e42f6d4f3af195526c2 ;;
v8) install_version v8 v8.3.3.23+4 01abfb3e13dae2066c9fcdc9fd3a3a21cd08212feb7ee2f927d8acaa5c3e560f8ce7c78c533c6aad7048aaecc14f7445891f06cb38a1720e1637a971c0a02295 ;;
x2t) install_x2t v7.3+1 ab0c05b0e4c81071acea83f0c6a8e75f5870c360ec4abc4af09105dd9b52264af9711ec0b7020e87095193ac9b6e20305e446f2321a541f743626a598e5318c1 ;;
*)
echo "Unknown version: $version"

View File

@ -86,4 +86,28 @@ removeBlock.complete = function (Env, body, cb) {
Block.removeLoginBlock(Env, publicKey, reason, edPublic, cb);
};
// Get an upload cookie
// Get a cookie allowing you to upload to the blobstage of your user
const uploadCookie = Commands.UPLOAD_COOKIE = function (Env, body, cb) {
const { publicKey } = body;
// they must provide a valid public key
if (publicKey && typeof(publicKey) === "string"
&& publicKey.length === 44) {
return cb();
}
cb("INVALID_KEY");
};
uploadCookie.complete = function (Env, body, cb) {
const { publicKey } = body;
const safeKey = Util.escapeKeyCharacters(publicKey);
Env.blobStore.uploadCookie(safeKey, (err, cookie) => {
if (err) { return void cb(err); }
cb(void 0, {cookie});
});
};

View File

@ -196,7 +196,6 @@ Quota.shouldContactServer = function (Env) {
(
typeof(Env.blockDailyCheck) === 'undefined' &&
Env.adminEmail === false
&& Env.allowSubscriptions === false
)
);
};
@ -225,22 +224,23 @@ var queryQuotaServer = function (Env, cb) {
}
};
const accountsOrigin = Env.accounts_api;
var H = Https;
if (typeof(Env.accounts_api) === 'string') {
if (typeof(accountsOrigin) === 'string') {
try {
let url = new URL(Env.accounts_api);
let url = new URL(accountsOrigin);
if (!['https:', 'http:'].includes(url.protocol)) { throw new Error("INVALID_PROTOCOL"); }
if (url.protocol === 'http:') { H = Http; }
let port = Number(url.port);
if (port && typeof(port) === 'number') { options.port = port; }
options.host = url.hostname;
Env.Log.info("USING_CUSTOM_ACCOUNTS_API", {
value: Env.accounts_api,
value: accountsOrigin,
});
} catch (err) {
Env.Log.error("INVALID_CUSTOM_QUOTA_API", {
error: err.message,
value: Env.accounts_api,
value: accountsOrigin,
});
}
}
@ -282,9 +282,11 @@ Quota.queryQuotaServer = function (Env, cb) {
Quota.updateCachedLimits = function (Env, _cb) {
var cb = Util.mkAsync(_cb);
Quota.applyCustomLimits(Env);
if (!Env.allowSubscriptions || !Env.accounts_api) { return void cb(); }
if (!Env.accounts_api) {
Quota.applyCustomLimits(Env);
return void cb();
}
Quota.queryQuotaServer(Env, function (err, json) {
if (err) { return void cb(err); }
if (!json) { return void cb(); }

View File

@ -75,7 +75,7 @@ Upload.status = function (Env, safeKey, filesize, _cb) { // FIXME FILES
};
Upload.upload = function (Env, safeKey, chunk, cb) {
Env.blobStore.upload(safeKey, chunk, cb);
Env.blobStore.uploadWs(safeKey, chunk, cb);
};
Upload.cancel = function (Env, safeKey, arg, cb) {

View File

@ -103,7 +103,7 @@ module.exports.create = function (config) {
httpSafePort: httpSafePort,
websocketPort: config.websocketPort,
accounts_api: config.accounts_api || undefined, // this simplifies integration with an accounts page
accounts_api: config.accounts_api,
shouldUpdateNode: !isRecentVersion(),
@ -212,7 +212,6 @@ module.exports.create = function (config) {
See also the cached 'restrictRegistration' value in server.js#serveConfig
*/
restrictRegistration: false,
allowSubscriptions: config.allowSubscriptions === true,
blockDailyCheck: config.blockDailyCheck === true,
consentToContact: false,
@ -225,8 +224,6 @@ module.exports.create = function (config) {
instanceJurisdiction: {},
instanceNotice: {},
myDomain: config.myDomain,
mySubdomain: config.mySubdomain, // only exists for the accounts integration
customLimits: {},
// FIXME this attribute isn't in the default conf
// but it is referenced in Quota
@ -259,6 +256,14 @@ module.exports.create = function (config) {
selfDestructTo: {},
};
Object.keys(plugins || {}).forEach(name => {
let plugin = plugins[name];
if (!plugin.customizeEnv) { return; }
try { plugin.customizeEnv(Env); }
catch (e) {}
});
(function () {
var max = config.maxWorkers;
// if the supplied value is not a positive number, leave maxWorkers undefined

View File

@ -69,6 +69,7 @@ const NOAUTH = require("./challenge-commands/base.js");
COMMANDS.MFA_CHECK = NOAUTH.MFA_CHECK;
COMMANDS.WRITE_BLOCK = NOAUTH.WRITE_BLOCK; // Account creation + password change
COMMANDS.REMOVE_BLOCK = NOAUTH.REMOVE_BLOCK;
COMMANDS.UPLOAD_COOKIE = NOAUTH.UPLOAD_COOKIE;
const TOTP = require("./challenge-commands/totp.js");
COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;

View File

@ -19,6 +19,7 @@ const BlobStore = require("./storage/blob");
const BlockStore = require("./storage/block");
const plugins = require("./plugin-manager");
const gzipStatic = require('connect-gzip-static');
const CPCrypto = require('./crypto');
const DEFAULT_QUERY_TIMEOUT = 5000;
const PID = process.pid;
@ -26,6 +27,8 @@ const PID = process.pid;
let SSOUtils = plugins.SSO && plugins.SSO.utils;
var Env = JSON.parse(process.env.Env);
let blobStore;
let cpcrypto;
Env.plugins = plugins;
const response = Util.response(function (errLabel, info) {
if (!Env.Log) { return; }
@ -70,6 +73,7 @@ const EVENTS = {};
EVENTS.ENV_UPDATE = function (data /*, cb */) {
try {
Env = JSON.parse(data);
Env.blobStore = blobStore;
Env.Log = Log;
Env.plugins = plugins;
Env.sendMessage = sendMessage;
@ -263,7 +267,6 @@ app.use('/ssoauth', (req, res, next) => {
next();
});
app.use('/blob', function (req, res, next) {
/* Head requests are used to check the size of a blob.
Clients can configure a maximum size to download automatically,
@ -611,7 +614,7 @@ var serveConfig = makeRouteCache(function () {
urlArgs: 'ver=' + Env.version + cacheString(),
},
removeDonateButton: (Env.removeDonateButton === true),
allowSubscriptions: (Env.allowSubscriptions === true),
accounts_api: Env.accounts_api,
websocketPath: Env.websocketPath,
httpUnsafeOrigin: Env.httpUnsafeOrigin,
adminEmail: Env.adminEmail,
@ -631,7 +634,6 @@ var serveConfig = makeRouteCache(function () {
fileHost: Env.fileHost,
shouldUpdateNode: Env.shouldUpdateNode || undefined,
listMyInstance: Env.listMyInstance,
accounts_api: Env.accounts_api,
sso: ssoCfg,
enforceMFA: Env.enforceMFA,
onlyOffice: Env.onlyOffice
@ -774,6 +776,51 @@ app.get('/api/logo', function (req, res) {
});
});
app.post('/upload-blob', Express.json({limit:"500kb"}), (req, res) => {
const { chunk, sig, edPublic } = req.body;
if (!cpcrypto) {
return void res.status(500).send({error: 'NOCRYPTO'});
}
const forbidden = reason => {
return void res.status(403).send({error: reason});
};
try {
// Check signature
const sigu8 = Util.decodeBase64(sig);
const vkey = Util.decodeBase64(edPublic);
const ok = cpcrypto.open(sigu8, vkey);
if (!ok) { return forbidden('INVALID_KEY'); }
const cookie = Util.encodeUTF8(sigu8.subarray(64));
// Check cookie
const safeKey = Util.escapeKeyCharacters(edPublic);
Env.blobStore.checkUploadCookie(safeKey, value => {
if (value !== cookie) {
return forbidden('INVALID_COOKIE');
}
// Upload chunk
Env.blobStore.upload(safeKey, chunk, (err) => {
if (err) {
return res.status(500).send({error: err});
}
// Get new cookie
Env.blobStore.uploadCookie(safeKey, (err, _c) => {
if (err) {
return res.status(500).send({error: err});
}
res.status(200).send({
cookie: _c
});
});
});
});
} catch (e) {
return void res.status(500).send({error: e.message});
}
});
// This endpoint handles authenticated RPCs over HTTP
// via an interactive challenge-response protocol
app.use(Express.json());
@ -781,6 +828,7 @@ app.post('/api/auth', function (req, res, next) {
AuthCommands.handle(Env, req, res, next);
});
app.use(function (req, res /*, next */) {
if (/^(\/favicon\.ico\/|.*\.js\.map|.*\/translations\/.*\.json)/.test(req.url)) {
// ignore common 404s
@ -824,7 +872,10 @@ nThen(function (w) {
getSession: function () {},
}, w(function (err, blob) {
if (err) { return; }
Env.blobStore = blob;
Env.blobStore = blobStore = blob;
}));
CPCrypto.init(w(function (err, crypto) {
cpcrypto = crypto;
}));
}).nThen(function () {
// TODO inform the parent process that this worker is ready

View File

@ -14,8 +14,8 @@ Stats.instanceData = function (Env) {
version: Env.version,
installMethod: Env.installMethod,
domain: Env.myDomain,
subdomain: Env.mySubdomain,
domain: Env.accounts_domain,
subdomain: Env.accounts_subdomain,
httpUnsafeOrigin: Env.httpUnsafeOrigin,
httpSafeOrigin: Env.httpSafeOrigin,

View File

@ -10,6 +10,8 @@ var BlobStore = module.exports;
var nThen = require("nthen");
var Semaphore = require("saferphore");
var Util = require("../common-util");
const Crypto = require('crypto');
const PERMISSIVE = 511;
const readFileBin = require("../stream-file").readFileBin;
@ -270,15 +272,17 @@ var readBlobMetadata = function (env, blobId, handler, _cb) {
/********** METHODS **************/
var upload = function (Env, safeKey, content, cb) {
var uploadWs = function (Env, safeKey, content, cb) {
var dec;
try { dec = Buffer.from(content, 'base64'); }
catch (e) { return void cb('DECODE_BUFFER'); }
var len = dec.length;
var session = Env.getSession(safeKey);
/*
if (typeof(session.currentUploadSize) !== 'number' ||
typeof(session.pendingUploadSize) !== 'number') {
// improperly initialized... maybe they didn't check before uploading?
@ -289,6 +293,7 @@ var upload = function (Env, safeKey, content, cb) {
if (session.currentUploadSize > session.pendingUploadSize) {
return cb('E_OVER_LIMIT');
}
*/
var stagePath = makeStagePath(Env, safeKey);
@ -300,15 +305,50 @@ var upload = function (Env, safeKey, content, cb) {
blobstage.write(dec);
session.currentUploadSize += len;
cb(void 0, dec.length);
//Env.incrementBytesWritten(len);
});
} else {
session.blobstage.write(dec);
session.currentUploadSize += len;
cb(void 0, dec.length);
//Env.incrementBytesWritten(len);
}
};
var upload = function (Env, safeKey, content, cb) {
var dec;
try { dec = Buffer.from(content, 'base64'); }
catch (e) { return void cb('DECODE_BUFFER'); }
var path = makeStagePath(Env, safeKey);
Fs.appendFile(path, dec, cb);
};
const getRandomCookie = function () {
return Crypto.randomBytes(16).toString('hex');
};
var uploadCookie = function (Env, safeKey, cb) {
var stagePath = makeStagePath(Env, safeKey);
var cookiePath = stagePath + '.cookie';
const cookie = getRandomCookie();
Fse.mkdirp(Path.dirname(cookiePath), PERMISSIVE, function (err) {
if (err && err.code !== 'EEXIST') { return void cb(err); }
Fs.writeFile(cookiePath, cookie, err => {
cb(err, cookie);
});
});
};
var checkUploadCookie = function (Env, safeKey, cb) {
var stagePath = makeStagePath(Env, safeKey);
var cookiePath = stagePath + '.cookie';
Fs.readFile(cookiePath, function (err, content) {
if (err) { return void cb(); }
let expireTime = +new Date() - (5*60*1000);
Fs.stat(cookiePath, function (err, stats) {
if (stats.mtime < expireTime) { return void cb(); }
cb(content.toString('utf8'));
});
});
};
var closeBlobstage = function (Env, safeKey) {
var session = Env.getSession(safeKey);
@ -370,6 +410,10 @@ var upload_complete = function (Env, safeKey, id, cb) {
// FIXME we could just move and handle the EEXISTS instead of the above block
Fse.move(oldPath, newPath, function (e) {
if (e) { return void cb('RENAME_ERR'); }
// clear upload cookie
Fs.unlink(oldPath+'.cookie', function () {});
cb(void 0, id);
});
});
@ -448,6 +492,9 @@ var owned_upload_complete = function (Env, safeKey, id, cb) {
// otherwise it worked...
}));
}).nThen(function () {
// clear upload cookie
Fs.unlink(oldPath+'.cookie', function () {});
// clean up their session when you're done
// call back with the blob id...
cb(void 0, id);
@ -734,11 +781,26 @@ BlobStore.create = function (config, _cb) {
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
isFile(makeStagePath(Env, safeKey), cb);
},
uploadWs: function (safeKey, content, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
uploadWs(Env, safeKey, content, Util.once(Util.mkAsync(cb)));
},
upload: function (safeKey, content, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
upload(Env, safeKey, content, Util.once(Util.mkAsync(cb)));
},
uploadCookie: function (safeKey, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
uploadCookie(Env, safeKey, Util.once(Util.mkAsync(cb)));
},
checkUploadCookie: function (safeKey, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
checkUploadCookie(Env, safeKey, Util.once(Util.mkAsync(cb)));
},
cancel: function (safeKey, fileSize, _cb) {
var cb = Util.once(Util.mkAsync(_cb));

3221
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -80,7 +80,7 @@ More information about this can be found in [our translation guide](/customize.d
The best places to reach the development team and the community are the [CryptPad Forum](https://forum.cryptpad.org) and the [Matrix chat](https://matrix.to/#/#cryptpad:matrix.xwiki.com)
The team is also on the fediverse: [@cryptpad@fosstodon.org](https://fosstodon.org/@cryptpad)
The team is also on the fediverse: [@cryptpad@xwiki.com](https://social.xwiki.com/@CryptPad)
# Team

View File

@ -1,44 +0,0 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
var Https = require('https');
var Config = require("../lib/load-config");
var Package = require("../package.json");
var body = JSON.stringify({
domain: Config.myDomain,
subdomain: Config.mySubdomain || null,
adminEmail: Config.adminEmail,
version: Package.version,
});
var options = {
host: 'accounts.cryptpad.fr',
path: '/api/getauthorized',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body)
}
};
Https.request(options, function (response) {
if (!('' + response.statusCode).match(/^2\d\d$/)) {
throw new Error('SERVER ERROR ' + response.statusCode);
}
var str = '';
response.on('data', function (chunk) {
str += chunk;
});
response.on('end', function () {
try {
var json = JSON.parse(str);
console.log(json);
} catch (e) {
throw new Error(e);
}
});
}).on('error', function (e) {
console.error(e);
}).end(body);

View File

@ -48,6 +48,12 @@ const factory = (Util, Hash, Realtime) => {
var oldHref = exp.getHref(data);
if (oldHref === href) { return; }
data.href = exp.cryptor.encrypt(href);
// Recompute roHref for form (auditor link)
const parsed = Hash.parsePadUrl(href);
if (parsed.type !== "form") { return; }
const secret = Hash.getSecrets(parsed.type,
parsed.hash, data.password);
data.roHref = '/' + parsed.type + '/#' + Hash.getViewHashFromKeys(secret);
});
};

View File

@ -423,7 +423,7 @@ const factory = (Sortify, UserObject, ProxyManager,
var initTempRpc = (clientId, cb) => {
if (store.rpc) { return void cb(store.rpc); }
var kp = Crypto.Nacl.sign.keyPair();
var keys = {
var keys = store.tempKeys = {
edPublic: Util.encodeBase64(kp.publicKey),
edPrivate: Util.encodeBase64(kp.secretKey)
};
@ -2501,7 +2501,7 @@ const factory = (Sortify, UserObject, ProxyManager,
// don't always call onCacheReady and onReady?
// it depends on what was required by the first tab
onAccountCacheReady(returned => {
store.returned ||= returned;
store.cacheReturned ||= returned;
cacheCb(returned);
});
onAccountReady(returned => {
@ -2566,7 +2566,7 @@ const factory = (Sortify, UserObject, ProxyManager,
} = drive;
onDriveCacheReady(() => {
cacheCb(store.returned);
cacheCb(store.cacheReturned || store.returned);
});
onDriveReady(() => {
cb(store.returned);
@ -2637,7 +2637,8 @@ const factory = (Sortify, UserObject, ProxyManager,
};
// If we load CryptPad for the first time from an existing pad, don't create a
// drive automatically.
var onNoDrive = function (clientId, cb, initRpc) {
var onNoDrive = function (clientId, _cb, initRpc) {
const cb = Util.once(_cb);
var andThen = function () {
// Initialize modules that can be used by pads
// and don't require account data
@ -2649,30 +2650,37 @@ const factory = (Sortify, UserObject, ProxyManager,
cb({});
});
};
if (initRpc) {
// In integration mode, we may need an RPC
return initTempRpc(clientId, getAnon);
}
getAnon();
// We need to know the HistoryKeeper ID
// to initialize the anon RPC
loadHK(err => {
if (err) { return; }
if (initRpc) {
// In integration mode, we may need
// an authenticated RPC
return initTempRpc(clientId, getAnon);
}
getAnon();
});
// If initRpc is true, we need to wait for a network
// before calling back
if (!store.network && !initRpc) { cb({}); }
};
// We need an anonymous RPC to be able to check if the pad exists and to get
// its metadata, so we have to create a network first.
if (!store.network) {
var wsUrl = NetConfig.getWebsocketURL();
return void Netflux.connect(wsUrl).then(function (network) {
// If we already haave a network (race condition), use the
// existing one and forget this one
store.networkPromise = Netflux.connect(wsUrl);
andThen();
return store.networkPromise.then((network) => {
if (store.network === network) { return; }
// If we already haave a network (race condition)
// use the existing one and forget this one
if (!store.network) { store.network = network; }
else {
network.disconnect();
network = store.network;
}
// We need to know the HistoryKeeper ID to initialize the anon RPC
loadHK(err => {
if (err) { return void cb(err); }
andThen();
});
}, function (err) {
console.error(err);
cb({error: 'OFFLINE'});
@ -2707,6 +2715,10 @@ const factory = (Sortify, UserObject, ProxyManager,
if (obj?.error) {
Feedback.send("NO_DRIVE_ERROR", true);
}
if (!!data.neverDrive) {
// Send temp RPC keys to the browser to allow upload
obj.tempKeys = store?.tempKeys;
}
cb(obj);
}, !!data.neverDrive);
}
@ -2752,6 +2764,12 @@ const factory = (Sortify, UserObject, ProxyManager,
startModules(clientId, ret, onInit);
});
});
Store.pad.onCacheReady(() => {
// If we're online, wait for onJoined
if (store.network) { return; }
// Otherwise, continue with cached drive
next();
});
Store.pad.onJoined(next);
onPadRejectedEvt.reg(next);
});
@ -2869,8 +2887,9 @@ const factory = (Sortify, UserObject, ProxyManager,
});
}
// If this is not the first tab (initialized is true), it means either we don't
// support offline or we're already online
// If this is not the first tab (initialized is true),
// it means either we don't support offline or we're
// already online
if (initialized) {
if (store.networkTimeout) {
postMessage(clientId, "LOADING_DRIVE", {

View File

@ -36,10 +36,12 @@ const init = (config) => {
// Create account secret from hash
const secret = Hash.getSecrets('drive', hash);
const network = config.store?.network ||
config.store?.networkPromise;
const listmapConfig = {
data: {},
websocketURL: NetConfig.getWebsocketURL(),
network: config.store?.network,
network: network,
channel: secret.channel,
readOnly: false,
validateKey: secret.keys?.validateKey || undefined,
@ -71,10 +73,14 @@ const init = (config) => {
}).on('cacheready', function (info) {
store.realtime = info.realtime;
store.offline = true;
store.networkPromise = info.networkPromise;
const hadPromise = !!store.networkPromise;
store.networkPromise ||= info.networkPromise;
store.cacheReturned = returned;
if (store.networkPromise && store.networkPromise.then) {
// Show error if we can't connect, but only if the accounts
// was required first
if (store.networkPromise && store.networkPromise.then
&& !hadPromise) {
// Check if we can connect
const to = setTimeout(function () {
store.networkTimeout = true;

View File

@ -221,14 +221,30 @@ const factory = (Messaging, Hash, Util, Crypto, Block) => {
var curve = msg.author;
var friend = ctx.store.proxy.friends && ctx.store.proxy.friends[curve];
if (!friend || typeof msg.content !== "object") { return void cb(true); }
Object.keys(msg.content).forEach(function (key) {
friend[key] = msg.content[key];
});
if (ctx.store.messenger) {
ctx.store.messenger.onFriendUpdate(curve);
const edPublic = msg.content.edPublic;
const todo = () => {
Object.keys(msg.content).forEach(function (key) {
friend[key] = msg.content[key];
});
if (ctx.store.messenger) {
ctx.store.messenger.onFriendUpdate(curve);
}
ctx.updateMetadata();
cb(true);
};
if (msg.content.badge && ctx.store.modules['badge']) {
return ctx.store.modules['badge'].listBadges({
edPublic
}, (list => {
if (!list.includes(msg.content.badge)) {
delete msg.content['badge'];
}
todo();
}));
}
ctx.updateMetadata();
cb(true);
todo();
};
// Encrypt the password under the right key before sending it via URL hash

View File

@ -21,6 +21,7 @@ import * as UserObject from '../../common/user-object.js';
import * as SF from './sharedfolder.js';
const onJoinedEvt: any = Util.mkEvent(true);
const onCacheReadyEvt: any = Util.mkEvent(true);
const _getMetadata: Callback = (ctx, clientId, data, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
@ -238,7 +239,7 @@ const _join: Callback = (ctx, clientId, data) => {
if (!["EDELETED","EEXPIRED","ERESTRICTED"].includes(type)) {
return;
}
Store.leavePad(null, data, function () {});
ctx.leavePad(null, data, function () {});
};
const conf = {
Cache: store.neverCache ? undefined : Cache,
@ -248,6 +249,7 @@ const _join: Callback = (ctx, clientId, data) => {
},
onCacheReady: () => {
postMessage(clientId, "PAD_CACHE_READY");
onCacheReadyEvt.fire();
},
onReady: pad => {
const padData = pad.metadata || {};
@ -465,7 +467,8 @@ const init = (config) => {
postMessage,
store,
Store,
myDeletions: []
myDeletions: [],
leavePad: (clientId, data, cb) => {}
};
const join: RpcCall = (clientId, data, cb) => {
@ -510,6 +513,7 @@ const init = (config) => {
dropChannel(ctx, data.channel);
cb();
};
ctx.leavePad = leave;
const getChannels = () => {
return ctx.channels;
@ -527,6 +531,7 @@ const init = (config) => {
leave,
removeClient,
onJoined: onJoinedEvt.reg,
onCacheReady: onCacheReadyEvt.reg,
getChannels
};
};

View File

@ -93,10 +93,34 @@ const Badge: BadgeModule<ModuleObject> = {
updateMetadata: config.updateMetadata
};
const _listBadges = (data, cb) => {
listBadges(ctx, data, undefined, cb);
};
// On ready, check if our badge is still valid
const Store = ctx.Store;
Store.onReadyEvt.reg(() => {
const md = Store.getMetadata(void 0, 'drive', () => {});
const myBadge:string = md?.user?.badge || "";
if (!myBadge) { return; }
listBadges(ctx, {}, undefined, all => {
if (!all.includes(myBadge)) {
const profile = ctx?.store?.modules?.profile;
profile?.execCommand(void 0, {
cmd: 'SET',
data: {
key: 'badge',
value: ''
}
}, () => {});
}
});
});
return {
listBadges: _listBadges,
removeClient: () => {},
execCommand: (clientId, obj, cb) => {
console.log('Exex command', clientId, obj);
const cmd = obj.cmd;
const data = obj.data;
if (cmd === 'LIST_BADGES') {

View File

@ -3,9 +3,11 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const factory = (Util, Hash, Constants, Realtime,
Listmap, Crypto, ChainPad) => {
Messaging, Listmap, Crypto, ChainPad) => {
var Profile = {};
const onReady = Util.mkEvent(true);
var initializeProfile = function (ctx, cb) {
var profile = ctx.profile;
if (!profile.edit || !profile.view) {
@ -54,6 +56,15 @@ const factory = (Util, Hash, Constants, Realtime,
if (!lm.proxy.edPublic) {
lm.proxy.edPublic = ctx.store.proxy.edPublic;
}
if (!lm.proxy.proof) {
let str = secret.channel;
let myIDu8 = Util.decodeUTF8(str);
let k = Util.decodeBase64(ctx.store.proxy.edPrivate);
let nacl = Crypto.Nacl;
let s = nacl.sign(myIDu8, k);
let signature = Util.encodeBase64(s);
lm.proxy.proof = signature;
}
if (ctx.onReadyHandlers.length) {
ctx.onReadyHandlers.forEach(function (f) {
try {
@ -62,6 +73,7 @@ const factory = (Util, Hash, Constants, Realtime,
});
ctx.onReadyHandlers = [];
}
onReady.fire();
}).on('change', [], function () {
ctx.emit('UPDATE', lm.proxy, ctx.clients);
});
@ -90,21 +102,26 @@ const factory = (Util, Hash, Constants, Realtime,
};
var setValue = function (ctx, data, cId, cb) {
var key = data.key;
var value = data.value;
if (!key) { return; }
ctx.listmap.proxy[key] = value;
Realtime.whenRealtimeSyncs(ctx.listmap.realtime, function () {
ctx.emit('UPDATE', ctx.listmap.proxy, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
if (key === 'badge') {
ctx.Store.set(null, {
key: ['profile', 'badge'],
value: value || undefined
}, () => {});
}
cb(ctx.listmap.proxy);
onReady.reg(() => {
var key = data.key;
var value = data.value;
if (!key) { return; }
ctx.listmap.proxy[key] = value;
Realtime.whenRealtimeSyncs(ctx.listmap.realtime, function () {
ctx.emit('UPDATE', ctx.listmap.proxy, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
if (key === 'badge') {
ctx.Store.set(null, {
key: ['profile', 'badge'],
value: value || undefined
}, () => {
Messaging.updateMyData(ctx.store);
ctx.updateMetadata();
});
}
cb(ctx.listmap.proxy);
});
});
};
@ -167,6 +184,7 @@ module.exports = factory(
require('../../common/common-hash'),
require('../../common/common-constants'),
require('../../common/common-realtime'),
require('../components/messaging'),
require('chainpad-listmap'),
require('chainpad-crypto'),
require('chainpad')

View File

@ -89,6 +89,7 @@ export interface PadObject {
onCorruptedCache: RpcCall,
getChannels: () => string[],
onJoined: any,
onCacheReady: any,
}
export interface Pad {
init: (config: PadConfig) => PadObject

View File

@ -171,7 +171,7 @@
color: @cryptpad_text_col;
border-radius: @variables_radius_L;
font-weight: normal;
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox):not(.fa-map-marker) {
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox):not(.fa-map-marker):not(.fa-align-justify) {
display: none;
}
.tui-full-calendar-icon {
@ -179,9 +179,46 @@
flex-shrink: 0;
}
.tui-full-calendar-popup-detail-item {
display: flex;
margin-bottom: 0.3rem;
align-items: center;
.event-location {
display: flex;
align-items: center;
}
.fa-align-justify {
margin-top: 3px;
}
&.tui-full-calendar-popup-detail-item-separate {
&>span {
display: flex;
width: 100%;
}
.event-description {
display: block;
line-height: 1.1em;
p, ul {
margin:0.1rem;
}
}
}
a {
margin:0.1rem;
border-radius: @variables_radius;
color: @cryptpad_color_link;
text-decoration: underline;
&:focus-visible{
outline: @variables_focus_style;
}
}
span {
overflow: hidden;
word-break: break-word;
white-space: nowrap;
text-overflow: ellipsis;
.event-description{
white-space: normal;
}
}
}
.tui-full-calendar-section-button-save {

View File

@ -97,6 +97,10 @@ define([
});
return;
}
if (k === "until") {
rrule += ";"+k.toUpperCase()+"="+getICSDate(r[k]);
return;
}
rrule += ";"+k.toUpperCase()+"="+r[k];
});
return rrule;

View File

@ -408,13 +408,16 @@ define([
str = `<a href="${l}" id="${uid}">${str}</a>`;
APP.nextLocationUid = uid;
}
let location_icon = h('i.fa.fa-map-marker.tui-full-calendar-icon', { 'aria-label': Messages.calendar_loc }, []);
return location_icon.outerHTML + str;
let location_icon = h('i.fa.fa-map-marker.tui-full-calendar-icon', { 'aria-hidden': true }, []);
return `<div class="event-location"> ${location_icon.outerHTML} ${str} </div>`;
},
popupDetailBody: function(schedule) {
var str = schedule.body;
delete APP.eventBody;
return diffMk.render(str, true);
let description_icon = h('i.fa.fa-align-justify.tui-full-calendar-icon', { 'aria-hidden': true }, []);
let description = diffMk.render(str, true);
return `${description_icon.outerHTML}<div class="event-description">${description}</div>`;
},
popupIsAllDay: function() { return Messages.calendar_allDay; },
titlePlaceholder: function() { return Messages.calendar_title; },
@ -2354,6 +2357,14 @@ APP.recurrenceRule = {
};
var onCalendarEditPopup = function (el) {
var $el = $(el);
const $header = $el.find('.tui-full-calendar-section-header');
$header.attr('id', 'tui-full-calendar-section-header');
$el.attr('aria-labelledby', 'tui-full-calendar-section-header');
const $desc = $el.find('.tui-full-calendar-section-detail');
$desc.attr('id', 'tui-full-calendar-section-detail');
$el.attr('aria-describedby', 'tui-full-calendar-section-detail');
$el.find('.tui-full-calendar-popup-edit').addClass('btn btn-primary');
$el.find('.tui-full-calendar-popup-edit .tui-full-calendar-icon').addClass('fa fa-pencil').removeClass('tui-full-calendar-icon');
$el.find('.tui-full-calendar-content').removeClass('tui-full-calendar-content');

View File

@ -82,7 +82,7 @@ define([
var trimmedSafe = trimSlashes(ApiConfig.httpSafeOrigin);
var trimmedUnsafe = trimSlashes(ApiConfig.httpUnsafeOrigin);
var fileHost = ApiConfig.fileHost;
var accounts_api = ApiConfig.accounts_api || AppConfig.accounts_api || undefined;
var accounts_api = ApiConfig.accounts_api || undefined;
var getAPIPlaceholderPath = function (relative) {
var absolute;
@ -113,15 +113,6 @@ define([
} catch (e) {}
}
var ACCOUNTS_URL;
try {
if (typeof(AppConfig.upgradeURL) === 'string') {
ACCOUNTS_URL = new URL(AppConfig.upgradeURL, trimmedUnsafe).origin;
}
} catch (err) {
console.error(err);
}
var debugOrigins = {
httpUnsafeOrigin: trimmedUnsafe,
httpSafeOrigin: trimmedSafe,
@ -1026,8 +1017,7 @@ define([
(HTTP_API_URL && HTTP_API_URL !== $outer) ? HTTP_API_URL : undefined,
isHTTPS(fileHost)? fileHost: undefined,
// support for cryptpad.fr configuration
accounts_api,
![trimmedUnsafe, trimmedSafe].includes(ACCOUNTS_URL)? ACCOUNTS_URL: undefined,
accounts_api
],
'img-src': ["'self'", 'data:', 'blob:', $outer],
@ -1066,8 +1056,7 @@ define([
API_URL.origin,
(HTTP_API_URL && HTTP_API_URL !== $outer) ? HTTP_API_URL : undefined,
isHTTPS(fileHost)? fileHost: undefined,
accounts_api,
![trimmedUnsafe, trimmedSafe].includes(ACCOUNTS_URL)? ACCOUNTS_URL: undefined,
accounts_api
],
'img-src': ["'self'", 'data:', 'blob:', $outer],
'media-src': ['blob:'],

View File

@ -179,6 +179,9 @@ define([
var isSmallScreen = () => window.innerWidth <= 600;
const showEditor = Util.once(() => {
$('#cp-app-code-editor').css('display', '');
});
var mkPreviewPane = function (editor, CodeMirror, framework, isPresentMode) {
var $previewContainer = $('#cp-app-code-preview');
var $preview = $('#cp-app-code-preview-content');
@ -345,8 +348,8 @@ define([
$codeMirrorContainer.addClass('cp-app-code-fullpage');
};
framework.onReady(function () {
showEditor();
handleResize();
window.addEventListener('resize', handleResize);
@ -536,6 +539,7 @@ define([
}
framework.onContentUpdate(function (newContent) {
showEditor();
var highlightMode = newContent.highlightMode;
if (highlightMode && highlightMode !== CodeMirror.highlightMode) {
CodeMirror.setMode(highlightMode, evModeChange.fire);
@ -578,6 +582,13 @@ define([
framework.setCursorGetter(CodeMirror.getCursor);
editor.on('cursorActivity', updateCursor);
editor.setOption("extraKeys", {
"Esc": function(cm, event) {
event.preventDefault();
document.body.focus();
}
});
framework.onEditableChange(function () {
editor.setOption('readOnly', framework.isLocked() || framework.isReadOnly());
});
@ -696,7 +707,7 @@ define([
h('div#cp-app-code-preview-content'),
h('div#cp-app-code-print')
])
]);
]).hide();
nThen(function (waitFor) {
$(waitFor());

View File

@ -212,6 +212,12 @@ define([
h('div'+cls, content),
])
]);
var dialogContent = frame.querySelector('div' + cls);
dialogContent.setAttribute('aria-live', 'assertive');
dialogContent.setAttribute('role', 'alertdialog');
dialogContent.setAttribute('aria-modal', 'true');
var $frame = $(frame);
frame.closeModal = function (cb) {
frame.closeModal = function () {}; // Prevent further calls
@ -578,6 +584,7 @@ define([
};
let addTabListener = UI.addTabListener = frame => {
$(frame).attr('role', 'dialog').attr('aria-modal', 'true');
// find focusable elements
let modalElements = $(frame).find('a, button, input, [tabindex]:not([tabindex="-1"]), textarea').filter(':visible').filter(':not(:disabled)');
@ -1080,6 +1087,7 @@ define([
$loading.css('display', '');
$loading.removeClass('cp-loading-hidden');
$loading.removeClass('cp-loading-transparent');
$loading.attr('aria-live','polite');
if (config.newProgress) {
var progress = h('div.cp-loading-progress', [
h('p.cp-loading-progress-list'),
@ -1127,6 +1135,13 @@ define([
$('head > link[href^="/customize/src/pre-loading.css"]').remove();
$('html').toggleClass('cp-loading-noscroll', false);
};
UI.emptyLoadingScreen = function (content) {
UI.addLoadingScreen();
var $loading = $('#' + LOADING);
$loading.find('.cp-loading-container').hide();
$loading.find('.cp-loading-logo').hide();
$loading.append(content);
};
UI.errorLoadingScreen = function (error, transparent, exitable) {
if (error === 'Error: XDR encoding failure') {
console.warn(error);
@ -1256,6 +1271,10 @@ define([
arrow: true,
maxWidth: '200px',
flip: true,
onShow: () => {
// Hide other tooltips
$('body').find('.tippy-popper').hide();
},
popperOptions: {
modifiers: {
preventOverflow: { boundariesElement: 'window' }

View File

@ -199,7 +199,9 @@ define([
}).filter(function (x) { return x; });
var noOthers = icons.length === 0 ? '.cp-usergrid-empty' : '';
var classes = noOthers + (config.large?'.large':'') + (config.list?'.list':'');
var classes = noOthers + (config.large?'.large':'') +
(config.list?'.list':'') +
(config.radio?'.radio':'');
var inputFilter = h('input', {
placeholder: Messages.share_filterFriend
@ -221,20 +223,24 @@ define([
$div.find('.cp-usergrid-user:not(.cp-selected):not([data-name*="'+name+'"])').hide();
}
};
$(inputFilter).on('keydown keyup change', redraw);
$(inputFilter).on('input keydown keyup change', redraw);
if (config.evOnFilter) {
config.evOnFilter.reg(redraw);
}
$(div).append(h('div.cp-usergrid-grid', icons));
if (!config.noSelect) {
$div.on('click', '.cp-usergrid-user', function () {
var sel = $(this).hasClass('cp-selected');
if (config.radio) {
$div.find('.cp-usergrid-user.cp-selected').removeClass('cp-selected');
}
if (!sel) {
$(this).addClass('cp-selected');
} else {
var order = $(this).attr('data-order');
order = order ? 'order:'+order : '';
$(this).removeClass('cp-selected').attr('style', order);
} else if (!config.radio) {
$(this).removeClass('cp-selected');
}
onSelect();
onSelect(!sel ? this : undefined);
});
$div.on('keydown', '.cp-usergrid-user', function (e) {
if (e.which === 13) {
@ -251,6 +257,55 @@ define([
};
};
UIElements.getUserTeamPicker = (common, config, onSelected) => {
const { msg, friendsData, teamsData } = config;
let teams;
const filter = h('input', {
placeholder: Messages.share_filterFriend
});
const evOnFilter = Util.mkEvent();
const contacts = UIElements.getUserGrid(Messages.contacts, {
common: common,
data: friendsData,
noFilter: false,
radio: true,
large: true,
evOnFilter
}, el => {
onSelected(el);
$(teams.div).find('.cp-selected').removeClass('cp-selected');
});
teams = UIElements.getUserGrid(Messages.teams, {
common: common,
data: teamsData,
noFilter: false,
radio: true,
large: true,
evOnFilter
}, el => {
onSelected(el);
$(contacts.div).find('.cp-selected').removeClass('cp-selected');
});
const $contactFilter = $(contacts.div).find('.cp-usergrid-filter input').hide();
const $teamFilter = $(teams.div).find('.cp-usergrid-filter input').hide();
$(filter).on('input', () => {
const val = filter.value;
$teamFilter.val(val);
$contactFilter.val(val);
evOnFilter.fire();
});
const content = h('div.cp-userteam-picker', [
h('div', msg),
h('div.cp-userteam-filter', filter),
contacts.div,
teams.div
]);
return content;
};
UIElements.noContactsMessage = function (common) {
var metadataMgr = common.getMetadataMgr();
@ -905,7 +960,8 @@ define([
case 'toggle':
button = $(h('button.cp-toolbar-tools', {
//title: data.title || '', // TODO display if the label text is collapsed
'aria-label': data.text || Messages.toolbar_tools // Fallback
'aria-label': data.text || Messages.toolbar_tools, // Fallback
'aria-pressed': false
}, [
h('i.fa.' + (data.icon || 'fa-wrench')),
h('span.cp-toolbar-name', data.text || Messages.toolbar_tools)
@ -923,6 +979,7 @@ define([
button.click(function (e) {
data.element.toggle();
var isVisible = data.element.is(':visible');
button.attr('aria-pressed', isVisible ? 'true' : 'false');
if (callback) { callback(isVisible); }
if (isVisible) {
button.addClass('cp-toolbar-button-active');
@ -1157,20 +1214,40 @@ define([
};
for (var k in actions) {
let $b = $('<button>', {
'data-notippy':1,
'data-type': k,
'class': 'pure-button fa ' + actions[k].icon,
title: Messages['mdToolbar_' + k] || k
}).click(onClick);
'class': 'pure-button cp-markdown-' + k,
'title': Messages['mdToolbar_' + k] || k,
'aria-label': Messages['mdToolbar_' + k] || k
}).append(
$('<i>', {
'class': 'fa ' + actions[k].icon,
'aria-hidden': 'true'
})).click(onClick);
if (k === "embed") { $toolbar.prepend($b); }
else { $toolbar.append($b); }
}
$('<button>', {
'class': 'pure-button fa fa-question cp-markdown-help',
title: Messages.mdToolbar_help
}).click(function () {
'data-notippy':1,
'class': 'pure-button cp-markdown-help',
'title': Messages.mdToolbar_help,
'aria-label': Messages.mdToolbar_help
}).append(
$('<i>', {
'class': 'fa fa-question',
'aria-hidden': 'true'
})).click(function () {
var href = Messages.mdToolbar_tutorial;
common.openUnsafeURL(href);
}).appendTo($toolbar);
$toolbar.on('keydown', function (e) {
if (e.key === 'Escape' || e.keyCode === 27) {
editor.focus();
e.preventDefault();
}
});
return $toolbar;
};
UIElements.createMarkdownToolbar = function (common, editor, opts) {
@ -1229,9 +1306,65 @@ define([
$toolbarButton.show();
};
function isSmallScreen() {
return window.innerHeight < 530 || window.innerWidth < 530;
}
var toolbarVisibleOnSmallScreen = false;
const $toolbarToggleButton = $(h('button.btn.cp-markdown-toggle-button', {
'aria-label': Messages.toolbar_show_text_tools,
'aria-pressed': 'false',
'data-notippy': 1,
'type': 'button',
'title': Messages.toolbar_show_text_tools
})).append([
h('i.fa.fa-wrench', { 'aria-hidden': 'true' }),
h('span.cp-toolbar-label', {}, Messages.toolbar_text_tools)
]).click(function () {
var isExpanded = $toolbar.is(':visible');
$toolbar.toggle();
$(this).toggleClass('cp-toolbar-button-active', !isExpanded)
.attr('aria-pressed', String(!isExpanded))
.attr('title', !isExpanded ? Messages.toolbar_hide_text_tools : Messages.toolbar_show_text_tools)
.attr('aria-label', !isExpanded ? Messages.toolbar_hide_text_tools : Messages.toolbar_show_text_tools);
toolbarVisibleOnSmallScreen = !isExpanded;
}).on('keydown keyup', e => {
// don't close modals when pressing Enter
// on the button
e.stopPropagation();
}).hide();
const updateToolbarVisibility = () => {
if (isSmallScreen()) {
$toolbarToggleButton.show();
if (toolbarVisibleOnSmallScreen) {
$toolbar.show();
$toolbarToggleButton.addClass('cp-toolbar-button-active')
.attr('aria-pressed', 'true');
} else {
$toolbar.hide();
$toolbarToggleButton.removeClass('cp-toolbar-button-active')
.attr('aria-pressed', 'false');
}
return;
}
$toolbarToggleButton.hide();
$toolbar.show();
};
if (opts?.toggleBar) {
$(window).on('resize', updateToolbarVisibility);
// Small delay to ensure the toolbar layout has rendered
// before checking for wrapping
setTimeout(updateToolbarVisibility);
}
return {
toolbar: $toolbar,
button: $toolbarButton,
toggleButton: $toolbarToggleButton[0],
setState: setState
};
};
@ -1279,14 +1412,16 @@ define([
common.fixLinks(text);
var closeButton = h('span.cp-help-close.fa.fa-times');
var closeButton = h('button.cp-help-close.fa.fa-times', {
title: Messages.help_close_button
});
var $toolbarButton = common.createButton('', true, {
text: Messages.help_button,
name: 'help'
}).addClass('cp-toolbar-button-active');
var help = h('div.cp-help-container', [
closeButton,
text
text,
closeButton
]);
$toolbarButton.attr('title', Messages.show_help_button);
@ -1318,7 +1453,6 @@ define([
text: text
};
};
/* Create a usage bar which keeps track of how much storage space is used
by your CryptDrive. The getPinnedUsage RPC is one of the heavier calls,
so we throttle its usage. Clients will not update more than once per
@ -1377,30 +1511,16 @@ define([
});
};
var makeUpgradeButton = function () {
var $a = $('<a>', {
'class': 'cp-limit-upgrade btn btn-success',
href: urls.upgradeURL,
rel: "noreferrer noopener",
target: "_blank",
}).text(Messages.upgradeAccount).appendTo($buttons);
$a.click(function () {
Feedback.send('UPGRADE_ACCOUNT');
});
};
if (!Config.removeDonateButton) {
if (!common.isLoggedIn() || !Config.allowSubscriptions) {
// user is not logged in, or subscriptions are disallowed
makeDonateButton();
} else if (!plan) {
// user is logged in and subscriptions are allowed
// and they don't have one. show upgrades
makeUpgradeButton();
makeDonateButton();
} else {
// they have a plan. show nothing
}
// Messages.upgradeAccount
common.getExtensionsSync('USAGE_BUTTON').some(ext => {
if (!ext.getButton) { return; }
let $b = ext.getButton(common, plan);
if (!$b) { return; }
$buttons.append($b);
});
// Add donate button
makeDonateButton();
}
var prettyUsage;
@ -2198,19 +2318,16 @@ define([
// section to determine if we have to manually hide a separator.
var surveyAlone = true;
if (Config.allowSubscriptions) {
Common.getExtensionsSync('USERMENU_ITEM').forEach(ext => {
if (!ext.getItem) {
return void console.error("Missing attribute for extension point", "USERMENU_ITEM", ext);
}
let item = ext.getItem(Common);
if (!item) { return; }
surveyAlone = false;
options.push({
tag: 'a',
attributes: {
'class': 'fa fa-star-o'
},
content: h('span', priv.plan ? Messages.settings_cat_subscription : Messages.pricing),
action: function () {
Common.openURL(priv.plan ? priv.accounts.upgradeURL :'/features.html');
},
});
}
options.push(item);
});
if (!priv.plan && !Config.removeDonateButton) {
surveyAlone = false;
options.push({
@ -2679,7 +2796,10 @@ define([
$creation.append(title);
if (early === 1) {
$creation.append(h('div.cp-creation-early.alert.alert-warning', Messages._getKey('premiumAccess', [
$creation.append(h('div.cp-creation-early.alert.alert-warning',{
role: 'alert',
'aria-live': 'assertive'
}, Messages._getKey('premiumAccess', [
domain
])));
}
@ -2777,12 +2897,15 @@ define([
// Password
let text;
if (type === 'form') {
text = h('div.cp-creation-password-warning.alert.alert-info.dismissable', h('span.cp-inline-alert-text', Messages.form_passwordWarning));
text = h('div.cp-creation-password-warning.alert.alert-info.dismissable',{
role: 'alert',
'aria-live': 'assertive'
}, h('span.cp-inline-alert-text', Messages.form_passwordWarning));
}
var password = h('div.cp-creation-password', [
UI.createCheckbox('cp-creation-password', Messages.properties_addPassword, false),
h('span.cp-creation-password-picker.cp-creation-slider', [
UI.passwordInput({id: 'cp-creation-password-val', placeholder: Messages.add_password})
UI.passwordInput({id: 'cp-creation-password-val', placeholder: Messages.properties_addPassword})
/*h('input#cp-creation-password-val', {
type: "text" // TODO type password with click to show
}),*/
@ -2794,10 +2917,10 @@ define([
var big = $w.width() > 800;
var right = h('button.fa.fa-chevron-right.cp-creation-template-more', {
'aria-label': Messages.next_templateList
'aria-label': Messages.page_next
});
var left = h('button.fa.fa-chevron-left.cp-creation-template-more', {
'aria-label': Messages.previous_templateList
'aria-label': Messages.page_previous
});
if (!big) {
$(left).removeClass('fa-chevron-left').addClass('fa-chevron-up');
@ -2830,7 +2953,6 @@ define([
var selected = 0; // Selected template in the list (highlighted)
var TEMPLATES_DISPLAYED = big ? 6 : 3; // Max templates displayed per page
var next = function () {}; // Function called when pressing tab to highlight the next template
var i = 0; // Index of the first template displayed in the current page
sframeChan.query("Q_CREATE_TEMPLATES", type, function (err, res) {
if (!res.data || !Array.isArray(res.data)) {
@ -2939,27 +3061,6 @@ define([
redraw(0);
}
// Change template selection when Tab is pressed
next = function (revert) {
var max = $creation.find('.cp-creation-template-element').length;
if (selected + 1 === max && !revert) {
selected = i + TEMPLATES_DISPLAYED < allData.length ? 0 : max;
return void redraw(i + TEMPLATES_DISPLAYED);
}
if (selected === 0 && revert) {
selected = i - TEMPLATES_DISPLAYED >= 0 ? TEMPLATES_DISPLAYED - 1 : 0;
return void redraw(i - TEMPLATES_DISPLAYED);
}
selected = revert ?
(--selected < 0 ? 0 : selected) :
++selected >= max ? max-1 : selected;
$creation.find('.cp-creation-template-element')
.removeClass('cp-creation-template-selected');
$($creation.find('.cp-creation-template-element').get(selected))
.addClass('cp-creation-template-selected');
};
$w.on('resize', function () {
var _big = $w.width() > 800;
if (big === _big) { return; }
@ -3262,7 +3363,6 @@ define([
]),
]);
UI.errorLoadingScreen(block, Boolean(cfg.loaded), Boolean(cfg.loaded));
$password.find('.cp-password-input').focus();
};
@ -3281,6 +3381,7 @@ define([
}, button),
]);
UI.errorLoadingScreen(block);
$(button).focus();
};
UIElements.getBurnAfterReadingWarning = function (common) {
var priv = common.getMetadataMgr().getPrivateData();

View File

@ -48,12 +48,12 @@ define([
}
};
const Env = {};
// Upgrade and donate URLs duplicated in pages.js
var origin = encodeURIComponent(window.location.hostname);
var common = window.Cryptpad = {
Messages: Messages,
donateURL: AppConfig.donateURL || "https://opencollective.com/cryptpad/",
upgradeURL: AppConfig.upgradeURL || 'https://accounts.cryptpad.fr/#/?on=' + origin,
account: {},
};
@ -79,6 +79,12 @@ define([
common.getAccessKeys = function (cb) {
var keys = [];
nThen(function (waitFor) {
// Not logged in? check for temp RPC keys
if (!LocalStore.isLoggedIn() && Env?.returned?.tempKeys) {
keys.push(Env.returned.tempKeys);
return;
}
// Push account keys
postMessage("GET", {
key: ['edPrivate'],
@ -499,8 +505,8 @@ define([
common.drive.onRemove = Util.mkEvent();
common.drive.onDeleted = Util.mkEvent();
// Profile
common.getProfileEditUrl = function (cb) {
postMessage("GET", { key: ['profile', 'edit'] }, function (obj) {
common.getProfileViewUrl = function (cb) {
postMessage("GET", { key: ['profile', 'view'] }, function (obj) {
cb(obj);
});
};
@ -631,7 +637,7 @@ define([
common.uploadChunk = function (teamId, data, cb) {
postMessage("UPLOAD_CHUNK", {teamId: teamId, chunk: data}, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
};
@ -1181,7 +1187,6 @@ define([
return;
}
let rtChannel, lastVersion, answersChannel;
let attributes = {};
nThen(function (waitFor) {
if (parsed.hashData.type !== 'pad') { return; }
@ -2738,6 +2743,7 @@ define([
console.log('Posting CONNECT');
postMessage('CONNECT', cfg, function (data) {
Env.returned = data;
// FIXME data should always exist
// this indicates a false condition in sharedWorker
// got here via a reference error:

View File

@ -660,25 +660,30 @@ define([
var $trashContextMenu = $("#cp-app-drive-context-trash");
$content.attr("tabindex", "0");
var splitter = h('div.cp-splitter', [
h('i.fa.fa-ellipsis-v')
]);
$contentContainer.append(splitter);
APP.$splitter = $(splitter).on('mousedown', function (e) {
e.preventDefault();
var x = e.pageX;
var w = $tree.width();
var handler = function (evt) {
if (evt.type === 'mouseup') {
$(window).off('mouseup mousemove', handler);
return;
}
$tree.css('width', (w - x + evt.pageX) + 'px');
};
$(window).off('mouseup mousemove', handler);
$(window).on('mouseup mousemove', handler);
});
if (APP.loggedIn) {
var splitter = h('div.cp-splitter', [
h('i.fa.fa-ellipsis-v')
]);
$contentContainer.append(splitter);
APP.$splitter = $(splitter).on('mousedown touchstart', function (e) {
e.preventDefault();
var x = e.type === 'touchstart' ? e.originalEvent.touches[0].pageX : e.pageX;
var w = $tree.width();
var handler = function (evt) {
if (evt.type === 'mouseup' || evt.type === 'touchend') {
$(window).off('mouseup mousemove touchend touchmove', handler);
return;
}
var pageX = evt.type === 'touchmove'
? evt.originalEvent.touches[0].pageX
: evt.pageX;
$tree.css('width', (w - x + pageX) + 'px');
};
$(window).off('mouseup mousemove touchend touchmove', handler, { passive: false });
$(window).on('mouseup mousemove touchend touchmove', handler);
});
}
// TOOLBAR
// DRIVE
@ -1757,7 +1762,11 @@ define([
var parsed = Hash.parsePadUrl(metadata.roHref);
// Forms: change "Open (read-only)" to "Open (as participant)"
if (parsed.type === "form") {
$('.cp-app-drive-context-openro .cp-text').text(Messages.fc_open_formro);
if (parsed.hashData.auditorKey) {
$('.cp-app-drive-context-openro .cp-text').text(Messages.fc_open_formaud);
} else {
$('.cp-app-drive-context-openro .cp-text').text(Messages.fc_open_formro);
}
}
}
}
@ -3462,19 +3471,23 @@ define([
b = [b];
}
if(a.length === 0 && b.length === 0) {
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(a[0] < b[0]) {
return -1;
} else if(a[0] > b[0]) {
return 1;
} else if (typeof (a[0]) !== typeof (b[0])) {
return String(a[0]) < String(b[0]) ? -1 : 1;
} else {
// This means `a[0] == b[0]`. Chop off the first elements and compare the rest.
return lexicographicCompare(a.slice(1), b.slice(1));
if (a[0] < b[0]) {
return -1;
} else if (a[0] > b[0]) {
return 1;
} else {
// This means `a[0] == b[0]`. Chop off the first elements and compare the rest.
return lexicographicCompare(a.slice(1), b.slice(1));
}
}
};
@ -3792,8 +3805,6 @@ define([
});
$element.contextmenu(openContextMenu('default'));
$element.data('context', 'default');
var $fileMenu = $('<li>').append($fileMenuIcon);
$element.append($fileMenu);
$container.append($element);
});
createGhostIcon($container);
@ -5118,6 +5129,11 @@ define([
}
var roParsed = Hash.parsePadUrl(data.roHref);
if (!auditorHash && roParsed.type === "form" &&
roParsed?.hashData?.auditorKey) {
// If we have stored the auditor hash, mark it as such
auditorHash = roParsed.hash;
}
var padType = parsed.type || roParsed.type;
var ro = !sf || (folders[el] && folders[el].version >= 2);
var padData = {

View File

@ -11,16 +11,21 @@ define([
const badges = {
admin: 'fa-star',
moderator: 'fa-life-ring',
premium: 'fa-check-circle',
premium: 'fa-ticket',
error: 'fa-exclamation-circle'
};
// safeBadges won't show an error when they aren't active anymore
// --> we just hide them
Badges.safeBadges = ["premium"];
Badges.render = id => {
let icon = badges[id];
if (!icon) { return; }
let cls = icon.indexOf('cptools') === 0 ? 'cptools '+icon : 'fa '+icon;
if (id === 'error') { cls += ' cp-badge-error'; }
return h('i', {
'data-badge': id,
'class': `cp-badge ${cls}`,
'title': Messages[`badges_${id}`] || id
});

View File

@ -10,13 +10,15 @@ define([
'/common/common-interface.js',
'/common/hyperscript.js',
'/common/media-tag.js',
'/common/inner/badges.js',
'/customize/messages.js',
'/customize/application_config.js',
'/components/croppie/croppie.min.js',
'/components/file-saver/FileSaver.min.js',
'css!/components/croppie/croppie.css',
], function ($, ApiConfig, Util, Hash, UI, h, MediaTag, Messages, AppConfig) {
], function ($, ApiConfig, Util, Hash, UI, h, MediaTag, Badges,
Messages, AppConfig) {
var MT = {};
// Configure MediaTags to use our local viewer
@ -148,8 +150,9 @@ define([
return text;
};
MT.displayAvatar = function (common, $container, href, name, _cb, uid) {
MT.displayAvatar = function (common, $container, href, name, _cb, uid, badge) {
var cb = Util.once(Util.mkAsync(_cb || function () {}));
const badgeEl = badge ? Badges.render(badge) : undefined;
var displayDefault = function () {
var animal_avatar;
if (uid && animal_avatars[uid]) {
@ -175,6 +178,7 @@ define([
'aria-hidden': true,
}).text(text);
$container.append($avatar);
$container.append(badgeEl);
if (uid && animal_avatar) {
animal_avatars[uid] = animal_avatar;
}
@ -187,6 +191,7 @@ define([
var nodes = $.parseHTML(avatars[href]);
var $el = $(nodes[0]);
$container.empty().append($el);
$container.append(badgeEl);
return void cb($el);
}
@ -223,6 +228,7 @@ define([
if (Util.bytesToMegabytes(data) > 0.5) { return void displayDefault(); }
var mt = UI.mediaTag(src, cryptKey);
var $img = $(mt).appendTo($container.empty());
$container.append(badgeEl);
MT.displayMediatagImage(common, $img, function (err, $image) {
if (err) { return void console.error(err); }
centerImage($img, $image);

View File

@ -631,6 +631,7 @@ define([
labelEdit, false, { mark: {tabindex:0} }),
auditor]),
burnAfterReading,
]);
// Burn after reading
@ -693,6 +694,9 @@ define([
: hashes.viewHash;
if (formAuditor && opts.auditorHash) {
hash = opts.auditorHash;
if (opts.hasPassword) {
hash += '/p';
}
}
var href = burnAfterReading ? opts.burnAfterReadingUrl
: (origin + pathname + '#' + hash);
@ -705,21 +709,24 @@ define([
});
return '<iframe src="' + url + '"></iframe>';
};
// disable edit share options if you don't have edit rights
if (versionHash) {
$rights.find('#cp-share-editable-false').attr('checked', true);
$rights.find('#cp-share-present').removeAttr('checked').attr('disabled', true);
$rights.find('#cp-share-editable-true').removeAttr('checked').attr('disabled', true);
} else if (!hashes.editHash) {
$rights.find('#cp-share-editable-false').attr('checked', true);
if (opts.auditorHash) {
$rights.find('#cp-share-editable-false').attr('checked', false).attr('disabled', true);
} else {
$rights.find('#cp-share-editable-false').attr('checked', true);
}
$rights.find('#cp-share-editable-true').removeAttr('checked').attr('disabled', true);
} else if (!hashes.viewHash) {
$rights.find('#cp-share-editable-false').removeAttr('checked').attr('disabled', true);
$rights.find('#cp-share-present').removeAttr('checked').attr('disabled', true);
$rights.find('#cp-share-editable-true').attr('checked', true);
}
if (isForm && !opts.auditorHash) {
if (isForm && !opts.auditorHash && opts.auditorHash) {
$rights.find('#cp-share-form').removeAttr('checked').attr('disabled', true);
}
@ -769,7 +776,12 @@ define([
$rights.find('#cp-share-editable-true').prop('checked', false);
$rights.find('#cp-share-present').prop('checked', true);
} else if ((val.edit === false && hashes.viewHash) || !hashes.editHash) {
$rights.find('#cp-share-editable-false').prop('checked', true);
if (opts.auditorHash) {
$rights.find('#cp-share-editable-false').prop('checked', false);
$rights.find('#cp-share-form').prop('checked', true);
} else {
$rights.find('#cp-share-editable-false').prop('checked', true);
}
$rights.find('#cp-share-editable-true').prop('checked', false);
$rights.find('#cp-share-present').prop('checked', false);
} else {
@ -786,6 +798,9 @@ define([
common.getMetadataMgr().onChange(function () {
// "hashes" is only available is the secure "share" app
var _hashes = common.getMetadataMgr().getPrivateData().hashes;
const h = _hashes.editHash || _hashes.viewHash;
const c = Hash.getSecrets('pad', h, opts.password).channel;
if (channel !== c) { return; }
if (!_hashes) { return; }
hashes = _hashes;
getLink().val(opts.getLinkValue());

View File

@ -8,9 +8,10 @@ define([
'/common/common-util.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/inner/badges.js',
'/common/hyperscript.js',
'/common/diffMarked.js',
], function ($, Messages, Util, UI, UIElements, h, DiffMd) {
], function ($, Messages, Util, UI, UIElements, Badges, h, DiffMd) {
'use strict';
var debug = console.log;
@ -74,7 +75,7 @@ define([
h('div.cp-app-contacts-category-content')
]),
h('div.cp-app-contacts-friends.cp-app-contacts-category', [
h('button.cp-app-contacts-muted-button', {tabindex:0},[
h('button.btn.btn-default.cp-app-contacts-muted-button', {tabindex:0},[
h('i.fa.fa-bell-slash'),
Messages.contacts_manageMuted
]),
@ -323,8 +324,13 @@ define([
});
var avatar = h('div.cp-avatar');
var avatarDiv = h('div.cp-avatar-container', avatar);
var headerContent = [avatar, moreHistory, data.isFriendChat ? removeHistory : undefined];
var headerContent = [
avatarDiv,
moreHistory,
data.isFriendChat ? removeHistory : undefined
];
if (isApp) {
headerContent = [
h('div.cp-app-contacts-header-title', Messages.contacts_padTitle),
@ -364,13 +370,14 @@ define([
var friend = contactsData[curvePublic] || {};
if (friend.avatar && avatars[friend.avatar]) {
$avatar.append(avatars[friend.avatar]).append(rightCol);
$avatar.append(Badges.render(friend.badge));
} else {
common.displayAvatar($avatar, friend.avatar, displayName, function ($img) {
if (friend.avatar && $img) {
avatars[friend.avatar] = $img[0].outerHTML;
}
$(rightCol).insertAfter($avatar);
}, friend.uid);
}, friend.uid, friend.badge);
}
var sending = false;
@ -507,6 +514,7 @@ define([
execCommand('MUTE_USER', {
curvePublic: data.curvePublic,
name: data.displayName || data.name,
badge: data.badge,
avatar: data.avatar
}, function (e /*, removed */) {
if (e) { return void console.error(e); }
@ -610,16 +618,18 @@ define([
});
});
const $avatar = $(h('div.cp-avatar')).appendTo($room);
if (friendData.avatar && avatars[friendData.avatar]) {
$room.append(avatars[friendData.avatar]);
$avatar.append(avatars[friendData.avatar]);
$avatar.append(Badges.render(friendData.badge));
$room.append(rightCol);
} else {
common.displayAvatar($room, friendData.avatar, room.name, function ($img) {
common.displayAvatar($avatar, friendData.avatar, room.name, function ($img) {
if (friendData.avatar && $img) {
avatars[friendData.avatar] = $img[0].outerHTML;
}
$room.append(rightCol);
}, friendData.uid);
}, friendData.uid, friendData.badge);
}
$room.append(status);
return $room;
@ -891,13 +901,13 @@ define([
.find('.cp-unmute-icon').show();
var data = muted[curve];
var avatar = h('span.cp-avatar');
var button = h('button', {
var button = h('button.btn', {
'data-user': curve
}, [
h('i.fa.fa-bell'),
Messages.contacts_unmute || 'unmute'
]);
common.displayAvatar($(avatar), data.avatar, data.name, Util.noop, data.uid);
common.displayAvatar($(avatar), data.avatar, data.name, Util.noop, data.uid, data.badge);
$(button).click(function () {
unmuteUser(curve, button);
execCommand('UNMUTE_USER', curve, function (e, data) {

View File

@ -296,6 +296,21 @@ define([
var last = JSON.parse(JSON.stringify(hashes[lastIndex]));
return last;
};
var deleteLastCp = function (i) {
var hashes = content.hashes;
if (!hashes || !Object.keys(hashes).length) { return {}; }
i = i || 0;
var idx = sortCpIndex(hashes);
var lastIndex = idx[idx.length - 1 - i];
if (typeof(lastIndex) === "undefined" || !hashes[lastIndex]) {
return;
}
delete hashes[lastIndex];
APP.onLocal();
APP.realtime.onSettle(function () {
UI.log(Messages.saved);
});
};
var rtChannel = {
ready: false,
@ -631,20 +646,6 @@ define([
});
}
};
var deleteLastCp = function () {
var hashes = content.hashes;
if (!hashes || !Object.keys(hashes).length) { return; }
var i = 0;
var idx = Object.keys(hashes).map(Number).sort(function (a, b) {
return a-b;
});
var lastIndex = idx[idx.length - 1 - i];
delete content.hashes[lastIndex];
APP.onLocal();
APP.realtime.onSettle(function () {
UI.log(Messages.saved);
});
};
var restoreLastCp = function () {
content.saveLock = myOOId;
APP.onLocal();
@ -721,7 +722,8 @@ define([
var u8 = new Uint8Array(arrayBuffer);
FileCrypto.decrypt(u8, key, function (err, decrypted) {
if (err) {
if (err === "DECRYPTION_ERROR") {
if (err === "DECRYPTION_ERROR" ||
err === "E_METADATA_DECRYPTION") {
console.warn(err);
return void reject(err);
}
@ -1088,6 +1090,7 @@ define([
if (content.version <= 2) {
setEditable(false, true);
}
console.log("OO loading");
};
@ -2122,6 +2125,20 @@ define([
}];
common.checkTrimHistory(channels);
}
console.log("OO ready");
};
const copy = (a, b) => {
Object.keys(b).forEach(k => {
if (k === "user") { return; } // Don't change user values
if (a[k]) {
if (typeof(a[k]) === "object" && typeof(b[k]) === "object") {
copy(a[k], b[k]);
}
return;
}
a[k] = b[k];
});
};
const createOOConfig = function(blob, file, lock, fromContent, lang, force) {
@ -2143,7 +2160,7 @@ define([
permissions: {
download: dc?.permissions?.download || false,
print: dc?.permissions?.print || true,
protect: false
protect: file.type === 'xlsx',
}
},
"documentType": file.doc,
@ -2173,18 +2190,6 @@ define([
}
};
let copy = (a, b) => {
Object.keys(b).forEach(k => {
if (k === "user") { return; } // Don't change user values
if (a[k]) {
if (typeof(a[k]) === "object" && typeof(b[k]) === "object") {
copy(a[k], b[k]);
}
return;
}
a[k] = b[k];
});
};
if (integrationConfig) {
let ec = integrationConfig.editorConfig;
let c = ooconfig.editorConfig.customization;
@ -2790,27 +2795,33 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
}, 100);
};
var loadDocument = function (noCp, useNewDefault, i) {
var loadDocument = function (noCp, useNewDefault, i, cb) {
if (ooLoaded) { return; }
var type = common.getMetadataMgr().getPrivateData().ooType;
var file = getFileType();
if (!noCp) {
var lastCp = getLastCp(false, i);
// If the last checkpoint is empty, load the "initial" doc instead
if (!lastCp || !lastCp.file) { return void loadDocument(true, useNewDefault); }
if (!lastCp || !lastCp.file) { return void loadDocument(true, useNewDefault, undefined, cb); }
// Load latest checkpoint
return void loadLastDocument(lastCp)
.then(({blob, fileType}) => {
startOO(blob, fileType);
cb({
blob,
file: fileType
});
})
.catch(() => {
// Checkpoint error: load the previous one
deleteLastCp(i);
i = i || 0;
loadDocument(noCp, useNewDefault, ++i);
loadDocument(noCp, useNewDefault, ++i, cb);
});
}
var blob = loadInitDocument(type, useNewDefault);
startOO(blob, file);
cb({
blob, file
});
};
var initializing = true;
@ -3519,6 +3530,8 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
var useNewDefault = content.version && content.version >= 2;
loadDocument(newDoc, useNewDefault,void 0, cpObj => {
openRtChannel(Util.once(function () {
setMyId();
oldHashes = JSON.parse(JSON.stringify(content.hashes));
@ -3582,7 +3595,8 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
}
var next = function () {
loadDocument(newDoc, useNewDefault);
const { blob, file } = cpObj;
startOO(blob, file);
setEditable(!readOnly);
UI.removeLoadingScreen();
};
@ -3774,6 +3788,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
next();
}));
});
};
config.onError = function (err) {

View File

@ -3,15 +3,19 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
define([
'/api/config',
'/file/file-crypto.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/cache-store.js',
'/common/outer/http-command.js',
'/components/chainpad-crypto/crypto.js',
'/components/nthen/index.js',
], function (FileCrypto, Hash, Util, Cache, nThen) {
], function (ApiConfig, FileCrypto, Hash, Util, Cache, ServerCommand, Crypto, nThen) {
var module = {};
const USE_WS = false;
module.uploadU8 =function (common, data, cb) {
module.uploadU8 = function (common, data, cb) {
var teamId = data.teamId;
var u8 = data.u8;
var metadata = data.metadata;
@ -27,13 +31,51 @@ define([
var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata);
var sendChunk = function (box, cb) {
var sendChunkWs = function (box, cb) {
var enc = Util.encodeBase64(box);
common.uploadChunk(teamId, enc, function (e, msg) {
cb(e, msg);
});
};
let uploadUrl = '/upload-blob';
let keys, cookie;
if (ApiConfig.fileHost) {
const origin = new URL(ApiConfig.fileHost).origin;
uploadUrl = origin + uploadUrl;
}
var sendChunk = function (box, cb) {
const enc = Util.encodeBase64(box);
const c = Util.decodeUTF8(cookie);
const sig_str = window.nacl.sign(c, keys.secretKey);
const sig = Util.encodeBase64(sig_str);
const body = {
chunk: enc,
sig: sig,
edPublic: keys.edPublic
};
fetch(uploadUrl, {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}).then(res => res.json())
.then(json => {
if (json?.error) {
return cb(json.error);
}
cookie = json.cookie;
cb();
}).catch(cb);
};
var actual = 0;
var encryptedArr = [];
var again = function (err, box) {
@ -45,7 +87,11 @@ define([
progressValue = Math.min(progressValue, 100);
updateProgress(progressValue);
return void sendChunk(box, function (e) {
let send = sendChunk;
if (USE_WS) {
send = sendChunkWs;
}
return void send(box, function (e) {
if (e) { return console.error(e); }
next(again);
});
@ -69,6 +115,30 @@ define([
});
};
const startUpload = () => {
if (USE_WS) {
return void next(again);
}
common.getAccessKeys(arr => {
const myKeys = arr.find(obj => {
return (!obj.id && !teamId) || +obj.id === +teamId;
});
if (!myKeys) { return void onError('NO_KEYS'); }
keys = {
edPublic: myKeys.edPublic,
publicKey: Util.decodeBase64(myKeys.edPublic),
secretKey: Util.decodeBase64(myKeys.edPrivate)
};
ServerCommand(keys, {
command: 'UPLOAD_COOKIE',
}, (err, data) => {
cookie = data?.cookie;
if (err || !cookie) { return void onError(err || 'NOCOOKIE'); }
next(again);
});
});
};
common.uploadStatus(teamId, estimate, function (e, pending) {
if (e) {
console.error(e);
@ -83,11 +153,11 @@ define([
if (e) {
return void console.error(e);
}
next(again);
startUpload();
});
});
}
next(again);
startUpload();
});
};

View File

@ -776,8 +776,13 @@ define([
};
var setFileImporter = function (options, fi, async) {
if (readOnly) { return; }
const priv = cpNfInner.metadataMgr.getPrivateData();
const isReadOnlyIntegration = priv.isNewFile &&
Boolean(priv.integrationConfig) &&
priv.initialState;
if (readOnly && !isReadOnlyIntegration) { return; }
fileImporter = function (c, f) {
console.error(state, STATE.READY, unsyncMode);
if (state !== STATE.READY || unsyncMode) {
return void UI.warn(Messages.disconnected);
}

View File

@ -123,6 +123,7 @@ define([
var password, newPadPassword, newPadPasswordForce;
var initialPathInDrive;
var burnAfterReading;
var parsedUnsafeLink;
var Handler;
var currentPad = window.CryptPad_location = {
@ -631,6 +632,7 @@ define([
// Use the same options in the full hash
var opts = parsed.getOptions();
parsed = Utils.Hash.parsePadUrl(newHref);
parsedUnsafeLink = Utils.Hash.parsePadUrl(newHref);
currentPad.href = parsed.getUrl(opts);
currentPad.hash = parsed.hashData && parsed.hashData.getHash(opts);
}
@ -799,7 +801,7 @@ define([
origin: window.location.origin,
pathname: window.location.pathname,
fileHost: ApiConfig.fileHost,
readOnly: readOnly,
readOnly: cfg?.integrationConfig?.readOnly || readOnly,
isTemplate: isTemplate,
newTemplate: Array.isArray(Cryptpad.initialPath)
&& Cryptpad.initialPath[0] === "template",
@ -813,8 +815,7 @@ define([
isHistoryVersion: parsed.hashData && parsed.hashData.versionHash,
notifications: notifs,
accounts: {
donateURL: Cryptpad.donateURL,
upgradeURL: Cryptpad.upgradeURL
donateURL: Cryptpad.donateURL
},
isNewFile: isNewFile,
isDeleted: isDeleted,
@ -894,7 +895,7 @@ define([
for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }
if (cfg.addData) {
cfg.addData(metaObj.priv, Cryptpad, metaObj.user, Utils);
cfg.addData(metaObj.priv, Cryptpad, metaObj.user, Utils, parsedUnsafeLink);
}
if (metaObj && metaObj.priv && typeof(metaObj.priv.plan) === "string") {
@ -2074,7 +2075,6 @@ define([
sframeChan.on('Q_ASK_NOTIFICATION', function (data, cb) {
if (!Utils.Notify.isSupported()) { return void cb(false); }
// eslint-disable-next-line compat/compat
Notification.requestPermission(function (s) {
cb(s === "granted");
});

View File

@ -6,7 +6,7 @@
const factory = function (Channel, NodeWS) {
let USE_MIN = true;
if (typeof(localStorage) !== "undefined" &&
localStorage.CryptPad_dev === "1") { USE_MIN = false; }
localStorage.CryptPad_noMin === "1") { USE_MIN = false; }
let path = '/common/worker.bundle.js?';
if (USE_MIN) { path = '/common/worker.bundle.min.js?'; }

View File

@ -255,7 +255,7 @@ MessengerUI, Messages, Pages, PadTypes) {
editUsersNames.forEach(function (data) {
var name = data.name || Messages.anonymous;
var safeName = Util.fixHTML(name);
var $span = $('<span>', {'class': 'cp-avatar'});
var $span = $('<span>', {'class': 'cp-userlist-entry'});
if (data.color && showColors) {
$span.css('border-color', data.color);
}
@ -369,22 +369,27 @@ MessengerUI, Messages, Pages, PadTypes) {
Common.openURL(origin+'/profile/#' + data.profile);
});
}
Common.displayAvatar($span, data.avatar, name, function () {
const spanAvatar = h('span.cp-avatar');
const $avatar = $(spanAvatar).prependTo($span);;
Common.displayAvatar($avatar, data.avatar, name, function () {
$span.append($rightCol);
}, data.uid);
$span.data('uid', data.uid);
if (false && data.badge && data.edPublic) { // XXX 2025.6
if (data.badge && data.edPublic) {
const addBadge = (badge) => {
let i = Badges.render(badge);
if (!i) { return; }
$rightCol.append(h('div.cp-userlist-badge', i));
//$rightCol.append(h('div.cp-userlist-badge', i));
$avatar.append(i);
};
const key = data.signature + '-' + data.badge;
const v = validatedBadges[key];
if (typeof (v) === "string") {
addBadge(v);
} else if (v === false) {
addBadge('error');
if (!Badges.safeBadges.includes(data.badge)) {
addBadge('error');
}
} else {
let ev = validatedBadges[key] ||= Util.mkEvent(true);
ev.reg(badge => { addBadge(badge); });
@ -397,6 +402,7 @@ MessengerUI, Messages, Pages, PadTypes) {
}, res => {
if (!res?.verified) {
validatedBadges[key] = false;
if (Badges.safeBadges.includes(data.badge)) { return; }
return void addBadge('error');
}
validatedBadges[key] = res.badge;
@ -999,17 +1005,16 @@ MessengerUI, Messages, Pages, PadTypes) {
if (e) { return void console.error("Unable to get the pinned usage", e); }
if (overLimit) {
$limit.show().click(function () {
if (ApiConfig.allowSubscriptions && Config.upgradeURL) {
var msg = Pages.setHTML(h('span'), Messages.pinLimitReachedAlert);
$(msg).find('a').attr({
target: '_blank',
href: Config.upgradeURL,
});
let handled = false;
// Msg.pinLimitReachedAlert
Common.getExtensionsSync('QUOTA_REACHED').forEach(ext => {
if (!ext.getAlert) { return; }
handled = true;
ext.getAlert();
});
if (handled) { return; }
UI.alert(msg);
} else {
UI.alert(Messages.pinLimitReachedAlertNoAccounts);
}
UI.alert(Messages.pinLimitReachedAlertNoAccounts);
});
}
};

View File

@ -1 +1,164 @@
{}
{
"type": {
"code": "کد",
"poll": "نظرسنجی",
"slide": "اسلایدهای مارک‌داون",
"file": "پرونده",
"media": "رسانه",
"presentation": "ارائه",
"diagram": "نمودار",
"whiteboard": "تخته سفید",
"kanban": "کانبان"
},
"padNotPinned": "این سند پس از ۳ ماه عدم فعالیت منقضی خواهد شد، {0}وارد شوید{1} یا {2}ثبت‌نام کنید{3} تا آن را حفظ کنید.",
"login_login": "ورود",
"logoutButton": "خروج",
"settingsButton": "تنظیمات",
"login_password": "گذرواژه",
"main_title": "کریپت‌پد: دانایی صفر، ویرایش هم‌زمان مشارکتی",
"typeError": "این سند با برنامه‌ی انتخاب‌شده سازگار نیست",
"onLogout": "شما از سیستم خارج شده‌اید، {0}اینجا کلیک کنید{1} تا وارد شوید<br>یا دکمه Esc را فشار دهید تا به سند خود در حالت فقط خواندنی دسترسی داشته باشید.",
"settings_logoutEverywhereButton": "خروج",
"common_connectionLost": "<b>اتصال به سرور از دست رفت</b><br>تا زمان بازگشت اتصال، در حالت فقط خواندنی هستید.",
"languageButton": "زبان",
"login_register": "نام‌نویسی",
"language": "زبان",
"mdToolbar_help": "کمک",
"share_linkEdit": "ویرایش",
"poll_edit": "ویرایش",
"login_username": "نام‌کاربری",
"mdToolbar_tutorial": "https://www.markdowntutorial.com/",
"edit": "ویرایش",
"formattedMB": "{0} مگابایت",
"formattedGB": "{0} گیگابایت",
"padNotPinnedVariable": "این سند پس از {4} روز عدم فعالیت منقضی خواهد شد. برای حفظ آن، {0}وارد شوید{1} یا {2}نام‌نویسی کنید{3}.",
"expiredError": "این سند به تاریخ انقضای خود رسیده و دیگر در دسترس نیست.",
"deletedError": "این سند حذف شده است و دیگر در دسترس نیست.",
"inactiveError": "این سند به دلیل عدم فعالیت حذف شده است. برای ایجاد یک سند جدید، کلید Esc را فشار دهید.",
"invalidHashError": "سندی که درخواست کرده‌اید دارای نشانی اینترنتی نامعتبر است.",
"errorCopy": " شما همچنان می‌توانید نسخه فعلی را در حالت فقط خواندنی با فشار دادن کلید Esc استفاده کنید.",
"errorRedirectToHome": "کلید Esc را فشار دهید تا به CryptDrive خود هدایت شوید.",
"loading": "در حال بارگیری...",
"error": "خطا",
"saved": "ذخیره شد",
"disabledApp": "این برنامه غیرفعال شده است. برای اطلاعات بیشتر با مدیریت این CryptPad تماس بگیرید.",
"disconnected": "اتصال قطع شد",
"MB": "مگابایت",
"GB": "گیگابایت",
"reconnecting": "در حال اتصال مجدد",
"typing": "در حال ویرایش",
"initializing": "در حال راه‌اندازی...",
"forgotten": "به سطل زباله منتقل شد",
"readonly": "فقط خواندنی",
"anonymous": "مهمان",
"users": "کاربران",
"editor": "ویرایشگر",
"upgrade": "ارتقا",
"upgradeAccount": "ارتقای حساب کاربری",
"storageStatus": "فضای ذخیره‌سازی:<br><b>{0}</b> استفاده شده از <b>{1}</b>",
"pinLimitReachedAlertNoAccounts": "شما به حد مجاز فضای ذخیره‌سازی خود رسیده‌اید",
"pinLimitNotPinned": "شما به حد مجاز فضای ذخیره‌سازی خود رسیده‌اید.<br>این سند در CryptDrive شما ذخیره نشده است.",
"importButton": "وارد کردن",
"importButtonTitle": "وارد کردن یک سند از یک پرونده محلی",
"exportButton": "خروجی گرفتن",
"exportButtonTitle": "خروجی گرفتن از این سند به یک پرونده محلی",
"exportPrompt": "چه نامی را برای پرونده خود انتخاب می‌کنید؟",
"user_rename": "تغییر نام نمایشی",
"user_displayName": "نام نمایشی",
"user_accountName": "حساب کاربری",
"saveTitle": "ذخیره عنوان (Enter)",
"forgetButton": "حذف",
"movedToTrash": "آن سند به سطل زباله منتقل شده است.<br><a>دسترسی به درایو من</a>",
"shareButton": "هم‌رسانی",
"newButton": "جدید",
"poll_comment_add": "افزودن دیدگاه",
"poll_comment_submit": "ارسال",
"poll_comment_remove": "حذف این دیدگاه",
"poll_comment_placeholder": "دیدگاه شما",
"fm_searchPlaceholder": "جستجو...",
"fm_newButton": "جدید",
"fc_hashtag": "برچسب‌ها",
"register_header": "نام‌نویسی",
"mdToolbar_link": "پیوند",
"features_anon": "مهمان",
"features_registered": "نام‌نویسی کرده",
"features_premium": "ویژه",
"creation_expireHours": "ساعت",
"creation_expireDays": "روز",
"creation_expireMonths": "ماه",
"creation_password": "گذرواژه\n",
"creation_create": "ایجاد",
"support_formButton": "ارسال",
"notifications_cat_all": "همه",
"notifications_cat_archived": "تاریخچه",
"settings_padNotifCheckbox": "غیرفعال کردن آگاه‌سازی‌های دیدگاه‌ها",
"broadcast_preview": "پیش‌نمایش آگاه‌سازی",
"newVersionError": "نسخه جدیدی از CryptPad در دسترس است.<br><a href='#'>بارگیری مجدد</a> کنید تا از نسخه جدید استفاده کنید یا کلید Escape را فشار دهید تا به محتوای خود در <b>حالت آفلاین</b> دسترسی داشته باشید.",
"pinLimitDrive": "شما به حد مجاز فضای ذخیره‌سازی خود رسیده‌اید.<br>نمی‌توانید اسناد جدیدی ایجاد کنید.",
"KB": "کیلوبایت",
"formattedKB": "{0} کیلوبایت",
"settings_notifCalendarTitle": "آگاه‌سازی‌های تقویم",
"settings_notifCalendarHint": "فعال/غیرفعال کردن همه آگاه‌سازی‌ها برای رویدادهای آینده تقویم.",
"settings_notifCalendarCheckbox": "فعال کردن آگاه‌سازی‌های تقویم",
"deleted": "حذف شد",
"mustLogin": "برای دسترسی به این صفحه باید وارد حساب کاربری شوید",
"errorState": "خطای بحرانی: {0}",
"userlist_offline": "شما در حال حاضر آفلاین هستید، فهرست کاربران در دسترس نیست.",
"poll_comment_disabled": "این نظرسنجی را با استفاده از دکمه ✓ منتشر کنید تا دیدگاه‌ها فعال شوند.",
"settings_padNotifTitle": "آگاه‌سازی‌های دیدگاه",
"admin_broadcastHint": "ارسال پیام به همه کاربران در این نمونه. تمام کاربران موجود و جدید آن را به عنوان یک آگاه‌سازی دریافت خواهند کرد. قبل از ارسال، پیام‌ها را با «پیش‌نمایش آگاه‌سازی» بررسی کنید. پیش‌نمایش آگاه‌سازی‌ها دارای یک نماد قرمز هستند و فقط برای شما قابل مشاهده‌اند.",
"pinLimitReached": "شما به حد مجاز فضای ذخیره‌سازی خود رسیده‌اید",
"share_withFriends": "هم‌رسانی",
"anonymousStoreDisabled": "مدیریت این نمونه CryptPad ذخیره‌سازی را برای مهمانان غیرفعال کرده است. برای دسترسی به CryptDrive شخصی خود وارد حساب کاربری شوید.",
"realtime_unrecoverableError": "یک خطای غیرقابل بازیابی رخ داده است. روی تایید کلیک کنید تا صفحه مجددا بارگیری شود.",
"synchronizing": "در حال همگام‌سازی",
"forgetPrompt": "با کلیک روی تایید، این سند به سطل زباله منتقل می‌شود. آیا مطمئن هستید؟",
"features_f_register": "نام‌نویسی رایگان",
"share_linkCategory": "پیوند",
"notificationsPage": "آگاه‌سازی‌ها",
"notifications_empty": "هیچ آگاه‌سازی جدیدی موجود نیست",
"openNotificationsApp": "باز کردن تابلو آگاه‌سازی‌ها",
"contacts_muteInfo": "شما هیچ آگاه‌سازی یا پیامی از کاربران بی‌صدا‌شده دریافت نخواهید کرد.<br>آن‌ها نمی‌دانند که شما آن‌ها را بی‌صدا کرده‌اید. ",
"comments_notification": "پاسخ‌ها به دیدگاه شما «{0}» در <b>{1}</b>",
"settings_padNotifHint": "نادیده گرفتن آگاه‌سازی‌ها هنگام پاسخ دادن فردی به یکی از دیدگاه‌های شما",
"allowNotifications": "فعال کردن آگاه‌سازی‌ها",
"form_allowNotifications": "آگاه‌سازی‌ها برای پاسخ‌های جدید",
"support_notificationsTitle": "غیرفعال کردن آگاه‌سازی‌ها",
"chainpadError": "خطای بحرانی در هنگام به‌روزرسانی محتوای شما رخ داده است. این صفحه در حالت فقط خواندنی قرار گرفته تا مطمئن شوید که کارتان از دست نمی‌رود.<br>کلید Esc را فشار دهید تا به مشاهده این سند ادامه دهید، یا صفحه را مجددا بارگیری کنید تا دوباره برای ویرایش امتحان کنید.",
"pinLimitReachedAlert": "شما به حد مجاز فضای ذخیره‌سازی خود رسیده‌اید. اسناد جدید در CryptDrive شما ذخیره نخواهند شد.<br>می‌توانید برخی اسناد را از CryptDrive خود حذف کنید یا <a>در یک طرح ویژه اشتراک بگیرید</a> تا حد مجاز خود را افزایش دهید.",
"clickToEdit": "برای ویرایش کلیک کنید",
"poll_comment_list": "دیدگاه‌ها",
"contacts_send": "ارسال",
"settings_title": "تنظیمات",
"admin_surveyHint": "افزودن، به‌روزرسانی یا حذف پیوندی به یک نظرسنجی خارجی. کاربران یک آگاه‌سازی دریافت خواهند کرد و این نظرسنجی از منوی کاربر در دسترس باقی خواهد ماند.",
"userListButton": "فهرست کاربران",
"chatButton": "گفت‌وگو",
"userAccountButton": "منوی کاربر",
"newButtonTitle": "ایجاد سند جدید",
"uploadButton": "بارگذاری پرونده‌ها",
"uploadFolderButton": "بارگذاری پوشه",
"uploadButtonTitle": "بارگذاری پرونده جدید در CryptDrive شما",
"presentButtonTitle": "ورود به حالت ارائه",
"backgroundButtonTitle": "تغییر رنگ پس‌زمینه در ارائه",
"colorButtonTitle": "تغییر رنگ متن در حالت ارائه",
"printText": "چاپ",
"printButton": "چاپ (enter)",
"printOptions": "گزینه‌های چیدمان",
"printSlideNumber": "نمایش شماره اسلاید",
"printDate": "نمایش تاریخ",
"printTitle": "نمایش عنوان سند",
"filePicker_close": "بستن",
"pad_mediatagPreview": "پیش‌نمایش",
"comments_deleted": "دیدگاه توسط نویسنده‌اش حذف شد",
"comments_edited": "ویرایش شد",
"comments_comment": "دیدگاه",
"comments_error": "نمی‌توانید در اینجا دیدگاه‌ی اضافه کنید",
"pad_settings_comments": "انتخاب کنید که دیدگاه‌ها به‌طور پیش‌فرض قابل مشاهده باشند یا پنهان شوند.",
"previewButtonTitle": "نمایش یا پنهان کردن حالت پیش‌نمایش مارک‌داون",
"printButtonTitle2": "چاپ سند شما یا وارد کردن آن به‌ عنوان یک پرونده PDF",
"oo_uploaded": "بارگذاری شما تکمیل شد. روی تایید کلیک کنید تا صفحه دوباره بارگیری شود یا لغو را انتخاب کنید تا در حالت فقط خواندنی ادامه دهید.",
"fm_newFile": "سند جدید",
"four04_pageNotFound": "صفحه‌ای که به‌ دنبال آن بودید پیدا نشد.",
"header_logoTitle": "رفتن به CryptDrive شما",
"properties_confirmNew": "آیا مطمئن هستید؟ افزودن گذرواژه باعث تغییر نشانی اینترنتی این سند و حذف تاریخچه آن می‌شود. کاربران بدون گذرواژه دسترسی به این سند را از دست خواهند داد"
}

View File

@ -1818,5 +1818,6 @@
"admin_addAdminsAdd": "Promovează un contact la admin",
"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"
"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."
}

View File

@ -1818,5 +1818,7 @@
"kanban_moveBoardRight": "Переместить доску вправо",
"moveItemUp": "Переместить элемент выше",
"moveItemDown": "Переместить элемент ниже",
"profile_remove_avatar": "Удалить этот аватар"
"profile_remove_avatar": "Удалить этот аватар",
"oo_couldNotConvertDocument": "Не удалось сконвертировать документ. Обратите внимание, что форматы Microsoft Office (.docx, .xlsx, .pptx) в настоящее время дают наилучшие результаты для импорта и экспорта.",
"oo_unstableMigrationWarning": "Импорт и экспорт некоторых форматов OpenDocument (.odt, .odp) в настоящее время нестабильны. Мы рекомендуем использовать другие форматы, где это возможно. Сейчас CryptPad попытается выполнить преобразование как можно лучше."
}

View File

@ -20,7 +20,7 @@
"diagram": "图表"
},
"common_connectionLost": "<b>服务器连接中断</b><br>您现在处于只读模式,直到连接恢复。",
"typeError": "此文档与所选应用不兼容",
"typeError": "此文档与所选应用程序不兼容",
"onLogout": "您已登出,{0}点击此处{1}登录<br>或按 Esc 键以只读模式访问您的文档。",
"loading": "正在加载…",
"error": "错误",
@ -32,9 +32,9 @@
"readonly": "只读",
"anonymous": "访客",
"users": "用户",
"viewer": "檢視者",
"viewers": "檢視群",
"editor": "編輯者",
"viewer": "查看者",
"viewers": "查看者",
"editor": "编辑者",
"language": "语言",
"upgrade": "升级",
"MB": "MB",
@ -57,7 +57,7 @@
"forgetPrompt": "点击确定会将此文档移至回收站。是否确定?",
"movedToTrash": "该文档已移至回收站。<br><a>访问我的云盘</a>",
"shareButton": "分享",
"shareSuccess": "已将链接复制到剪贴板",
"shareSuccess": "链接复制到剪贴板",
"newButton": "新",
"newButtonTitle": "创建新文档",
"saveTemplateButton": "另存为模板",
@ -85,8 +85,8 @@
"cancel": "取消",
"cancelButton": "取消Esc",
"historyButton": "显示文档历史记录",
"history_next": "下版本",
"history_prev": "先前版本",
"history_next": "下版本",
"history_prev": "上一版本",
"history_closeTitle": "关闭历史记录",
"history_restoreTitle": "还原所选文档版本",
"history_restorePrompt": "是否确定要用显示的版本替换文档的当前版本?",
@ -291,7 +291,7 @@
"profileButton": "个人资料",
"canvas_currentBrush": "当前画笔",
"canvas_imageEmbed": "从您的计算机嵌入图片",
"canvas_saveToDrive": "将此图片保存为 CryptDrive 中的文件",
"canvas_saveToDrive": "将此图片作为文件保存在 CryptDrive 中",
"canvas_widthLabel": "宽度:{0}",
"canvas_opacityLabel": "不透明度:{0}",
"oo_uploaded": "上传已完成。点击确定重新加载页面或点击取消以只读模式继续。",
@ -302,8 +302,8 @@
"poll_comment_remove": "删除此评论",
"poll_comment_add": "添加评论",
"poll_total": "总计",
"poll_bookmarked_col": "这是您的书签专栏。它将始终为您解锁并在开始时显示。",
"poll_bookmark_col": "将此列添加为书签,以便它始终解锁并在开始时为您显示",
"poll_bookmarked_col": "这是您的书签列。它将始终为您解锁并显示在开头。",
"poll_bookmark_col": "将此列加入书签,使其始终为您解锁并显示在开头",
"poll_unlocked": "已解锁",
"poll_locked": "已锁定",
"kanban_addBoard": "添加面板",
@ -322,11 +322,11 @@
"history_loadMore": "加载更多历史记录",
"show_help_button": "显示帮助",
"ok": "确定",
"fileEmbedTag": "然后将此媒体标签放置在您要嵌入的页面中的任何位置:",
"fileEmbedScript": "要嵌入此文件,请在您的页面中包含此脚本一次以加载媒体标签:",
"viewEmbedTag": "要嵌入此文档,请将此 iframe 包含在您想要的页面中的任何位置。您可以使用 CSS 或 HTML 属性对其进行样式设置。",
"themeButtonTitle": "选择用于代码和幻灯片编辑器的颜色主题",
"languageButtonTitle": "选择用于语法突出显示的语言",
"fileEmbedTag": "然后将此媒体标签放置在页面中您要嵌入的任意位置:",
"fileEmbedScript": "要嵌入此文件,请在页面中包含此脚本一次以加载媒体标签:",
"viewEmbedTag": "要嵌入此文档,请将此 iframe 包含在页面中所需的任意位置。您可以使用 CSS 或 HTML 属性对其进行样式设置。",
"themeButtonTitle": "选择代码和幻灯片编辑器使用的颜色主题",
"languageButtonTitle": "选择语法高亮使用的语言",
"slide_invalidLess": "自定义样式无效",
"tags_noentry": "您无法标记已删除的文档",
"tags_duplicate": "重复标签:{0}",
@ -365,7 +365,7 @@
"userlist_offline": "您当前处于离线状态,用户列表不可用。",
"errorState": "严重错误:{0}",
"realtime_unrecoverableError": "发生了不可恢复的错误,点击确定重新加载。",
"disabledApp": "已禁用此应用程序。如需了解详情,请联系此 CryptPad 的管理员。",
"disabledApp": "此应用程序已被禁用。如需了解详情,请联系此 CryptPad 的管理员。",
"mustLogin": "您必须登录才能访问此页面",
"deletedFromServer": "文档已销毁",
"newVersionError": "CryptPad 的新版本可用。<br><a href='#'>重新加载</a>以使用新版本,或按 Esc 键在<b>离线模式</b>下访问您的内容。",
@ -376,7 +376,7 @@
"inactiveError": "由于不活动,此文档已被删除。按 Esc 键创建新文档。",
"deletedError": "此文档已被删除,不再可用。",
"expiredError": "此文档已达到销毁日期,不再可用。",
"anonymousStoreDisabled": "此 CryptPad 实例的管理员已禁用访客存储服务。请登录以访问您自己的 CryptDrive。",
"anonymousStoreDisabled": "此 CryptPad 实例的管理员已禁用访客存储。请登录以访问您自己的 CryptDrive。",
"padNotPinnedVariable": "此文档将在 {4} 天不活动后过期,请{0}登录{1}或{2}注册{3}以保留它。",
"padNotPinned": "此文档将在 3 个月不活动后过期,请{0}登录{1}或{2}注册{3}以保留它。",
"download_step2": "解密中",
@ -562,7 +562,7 @@
"features_f_file0": "打开文档",
"features_f_core_note": "编辑、导入/导出、历史、用户列表、聊天",
"features_f_core": "普通功能",
"features_f_apps": "访问所有应用",
"features_f_apps": "访问所有应用程序",
"features_premium": "高级",
"features_registered": "已注册",
"features_anon": "访客",
@ -721,14 +721,14 @@
"share_copyProfileLink": "复制个人资料链接",
"share_noContactsLoggedIn": "您尚未与 CryptPad 上的任何人创建联系。 分享指向您的个人资料的链接,以便人们向您发送联系请求。",
"passwordFaqLink": "阅读有关密码的更多信息",
"share_embedPasswordAlert": "此项目受密码保护。 因为您与 CryptPad 联系人共享它,所以收件人不必输入密码。",
"share_embedPasswordAlert": "此项目受密码保护。嵌入此文档时,将要求查看者输入密码。",
"share_linkPasswordAlert": "此项目受密码保护。 当您发送链接时,收件人必须输入密码。",
"share_linkWarning": "此链接包含您文档的密钥。 收件人将获得对您的内容的不可撤销的访问权限。",
"pad_wordCount": "词:{0}",
"teams_table_role": "角色",
"teams_table_owners": "管理团队",
"teams_table_admins": "管理成员",
"teams_table_specificHint": "这些是旧的共享文件夹,查看者仍然编辑现有文档。 创建或复制到这些文件夹中的文档将具有标准权限。",
"teams_table_specificHint": "这些是旧的共享文件夹,查看者仍然有编辑现有文档的权限。创建或复制到这些文件夹中的文档将具有标准权限。",
"teams_table_specific": "例外",
"teams_table_generic_own": "管理团队:更改团队名称和头像、添加或删除所有者、更改团队订阅、删除团队。",
"teams_table_generic_admin": "编辑:创建、修改和删除文件夹和文档。",
@ -741,7 +741,7 @@
"password_error_seed": "找不到文档!<br>此错误可能由两个因素引起:添加/更改了密码,或者文档已从服务器中删除。",
"drive_sfPasswordError": "密码错误",
"drive_sfPassword": "您的共享文件夹 {0} 不再可用。 它要么已被其所有者删除,要么现在受到新密码的保护。 您可以从 CryptDrive 中删除此文件夹,或使用新密码恢复访问权限。",
"team_viewers": "观众",
"team_viewers": "查看者",
"settings_codeBrackets": "您团队的存储限制",
"team_title": "团队:{0}",
"team_demoteMeConfirm": "您即将放弃您的权力。您将无法撤消此操作。确定吗?",
@ -1026,7 +1026,7 @@
"form_answerChoice": "请选择您想如何回答此表单:",
"form_exportSheet": "导出到工作表",
"premiumAccess": "作为 {0} 的订阅者,您可以在此抢先体验应用进程中创建新文档。 请注意,它是实验性的,不应信任重要数据。",
"earlyAccessBlocked": "此应用程在此实例上尚不可用",
"earlyAccessBlocked": "此应用在此实例上尚不可用",
"premiumOnly": "在此应用进程中创建新文档目前仅限于 {0} 上的订阅者。 这是一个用于测试目的的早期访问实验应用进程。 它很快就会在 {0} 上提供给所有人。",
"download_step3": "正在转换…",
"fc_openIn": "在{0}打开",
@ -1402,7 +1402,7 @@
"ui_experimental": "此功能被认为是实验性的。",
"error_evalPermitted": "中止,因为不应允许 eval。\n\n此错误与 Content-Security-Policy 标头相关联,可能是由于:不支持它们的过时浏览器、干扰其正确行为的浏览器扩展或此 CryptPad 实例的错误配置。",
"error_incorrectAccess": "此页面只能通过 {0} 访问。",
"error_embeddingDisabledSpecific": "此 CryptPad 应用程禁用嵌入。",
"error_embeddingDisabledSpecific": "此 CryptPad 应用序已禁用嵌入。",
"error_embeddingDisabled": "此 CryptPad 实例禁用嵌入",
"admin_enableembedsHint": "允许将此实例中的文档和媒体嵌入其他网站。 这将在“共享”菜单中添加“嵌入”选项。 出于安全原因,即使此设置处于活动状态,也无法嵌入使用 OnlyOffice工作表、文档、演示文稿的应用。",
"admin_enableembedsTitle": "启用远程嵌入",
@ -1614,9 +1614,9 @@
"admin_channelPlaceholder": "已销毁的文档占位符",
"dph_default": "此内容不再可用",
"dph_account_destroyed": "所有者删除了此账号",
"dph_account_inactive": "此账号因未活动而被删除",
"dph_account_inactive": "此账号因不活动已被删除",
"dph_account_pw": "此账号的密码已更改",
"dph_pad_inactive": "此文档因未活动而被删除",
"dph_pad_inactive": "此文档因不活动已被删除",
"dph_account_moderated": "审核团队暂停了此账号",
"dph_pad_pw": "此文档受新密码保护",
"dph_tmp_destroyed": "所有者销毁了此模板",
@ -1818,5 +1818,7 @@
"toggleDrag": "拖动移动项目",
"error_limit": "请输入有效数字",
"admin_addKeyLabel": "使用管理员的公钥添加管理员",
"profile_remove_avatar": "移除此头像"
"profile_remove_avatar": "移除此头像",
"oo_couldNotConvertDocument": "文档转换失败。请注意Microsoft Office 格式(.docx、.xlsx、.pptx目前能获得最佳的导入和导出效果。",
"oo_unstableMigrationWarning": "部分 OpenDocument 格式(.odt、.odp的导入和导出目前还不稳定。我们建议尽可能使用其他格式。CryptPad 现在将尝试尽力转换。"
}

File diff suppressed because one or more lines are too long

View File

@ -135,6 +135,7 @@
document: blob,
ext: config.document.fileType,
autosave: config.events.onSave && (config.autosave || 10),
readOnly: config.mode === 'view',
editorConfig: config.editorConfig || {},
_config: serializedConfig()
}, function (obj) {
@ -269,6 +270,7 @@
* @param {function} events.onNewKey (data, callback) The function called when a new key is used.
* @param {function} events.onInsertImage (data, callback) The function called the user wants to add an image.
* @param {string} config.documentType The editor to load in CryptPad.
* @param {string} config.mode The editing state of the document to load ("view" or "edit", defaults to edit).
* @return {promise}
*/
var init = function (cryptpadURL, containerId, config) {

View File

@ -6,8 +6,8 @@
body.cp-app-diagram {
@import (once) "../../customize.dist/src/less2/include/browser.less";
@import (once) "../../customize.dist/src/less2/include/framework.less";
@import (once) "../../customize/src/less2/include/browser.less";
@import (once) "../../customize/src/less2/include/framework.less";
.framework_main(
@bg-color: @colortheme_apps[diagram],

View File

@ -10,7 +10,7 @@ define([
const blobToImage = (blob) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = function() {
reader.onloadend = function () {
resolve(reader.result);
};
reader.readAsDataURL(blob);
@ -35,7 +35,7 @@ define([
};
return {
main: function(userDoc, cb) {
main: function (userDoc, cb) {
delete userDoc.metadata;
const xml = DiagramUtil.jsonContentAsXML(userDoc);
@ -43,7 +43,7 @@ define([
let doc;
try {
doc = DiagramUtil.parseXML(xml);
} catch(e) {
} catch (e) {
console.error(e);
return;
}
@ -51,7 +51,15 @@ define([
const promises = loadCryptPadImages(doc);
Promise.all(promises).then(() => {
cb(new XMLSerializer().serializeToString(doc), '.drawio');
try {
const xmlString = new XMLSerializer().serializeToString(doc);
const blob = new Blob([xmlString], { type: 'application/xml' });
cb(blob, '.drawio');
} catch (error) {
console.error('Error exporting diagram:', error);
}
});
}
};

View File

@ -46,6 +46,7 @@ define([
var x2js = new X2JS();
var lastContent = x2js.xml2js(EMPTY_DRAWIO);
var drawIoInitalized = false;
$('#cp-app-diagram-editor').show();
var privateData = framework._.cpNfInner.metadataMgr.getPrivateData();
if (!privateData.isEmbed) {
@ -62,6 +63,9 @@ define([
var onDrawioInit = function() {
drawIoInitalized = true;
var readOnly = framework.isReadOnly() || framework.isLocked();
var grid = readOnly ? 0 : 1;
lastContent.mxfile.diagram.mxGraphModel._grid = grid;
var xmlStr = DiagramUtil.jsonContentAsXML(lastContent);
postMessageToDrawio({
action: 'load',
@ -146,20 +150,25 @@ define([
}, true
);
var parameters;
framework.onEditableChange(function () {
const editable = !framework.isLocked() && !framework.isReadOnly();
postMessageToDrawio({
action: 'spinner',
message: Messages.reconnecting,
show: !editable
});
var readOnly = framework.isReadOnly() || framework.isLocked();
if (readOnly) {
parameters.set('chrome', '0');
parameters.set('grid', '0');
} else {
parameters.set('chrome', '1');
parameters.set('grid', '1');
}
drawioFrame.src = ApiConfig.httpSafeOrigin + '/components/drawio/src/main/webapp/index.html?'
+ parameters;
});
// starting the CryptPad framework
framework.start();
drawioFrame.src = ApiConfig.httpSafeOrigin + '/components/drawio/src/main/webapp/index.html?'
+ new URLSearchParams({
parameters = new URLSearchParams({
test: 1,
stealth: 1,
embed: 1,
@ -185,6 +194,9 @@ define([
lang: Messages._languageUsed
});
drawioFrame.src = ApiConfig.httpSafeOrigin + '/components/drawio/src/main/webapp/index.html?'
+ parameters;
window.addEventListener("message", (event) => {
if (event.source === drawioFrame.contentWindow) {
var data = JSON.parse(event.data);
@ -197,6 +209,7 @@ define([
}, false);
};
$('#cp-app-diagram-editor').hide();
// Framework initialization
Framework.create({
toolbarContainer: '#cme_toolbox',

View File

@ -176,6 +176,16 @@
flex: 1;
justify-content: center;
min-width: 300px;
.cp-markdown-toggle-row {
margin: 0.5rem 0;
position: relative;
.cp-markdown-toggle-button {
.markdown-toolbar-button-style();
&.cp-toolbar-button-active {
//background-color: transparent !important;
}
}
}
div.timeline-container {
width: 100%;
@ -381,6 +391,12 @@
}
}
}
div.cp-form-response-msg-container {
.cp-markdown-toolbar {
background-color: transparent;
border-radius: @variables_radius @variables_radius 0 0;
}
}
}
div.cp-form-creator-results {
.cp-form-block {
@ -431,6 +447,14 @@
}
}
.cp-editor-wrapper {
.cp-form-edit-options-block {
padding: 1px;
overflow: hidden;
border: 1px solid @cp_forms-border;
}
}
.cp-form-creator-add-inline {
display: flex;
flex-flow: row;
@ -530,11 +554,6 @@
}
.cp-form-response-msg-container {
padding-bottom: 75px;
button {
white-space: initial;
line-height: 25px;
padding: 5.5px 6px;
}
.cp-form-response-msg-hint {
color: @cryptpad_color_link;
}
@ -819,6 +838,9 @@
display: flex;
flex-wrap: wrap;
align-items: baseline;
border-radius: @variables_radius;
margin-top: 0.5rem;
padding: 1px;
.CodeMirror {
cursor: default;
flex: 1;
@ -827,13 +849,16 @@
width: 80%;
min-height: 200px;
height: 200px;
border: 1px solid @cp_forms-border;
.CodeMirror-placeholder {
color: #777;
}
}
.cp-markdown-toolbar {
display: block;
padding: 0.1rem 0;
border-radius: @variables_radius 0;
width: 100%;
background-color: transparent;
}
}
.cp-form-edit-block {
@ -1358,8 +1383,12 @@
}
}
.cp-form-response-modal {
.CodeMirror {
.cp-form-markdown-editor-wrapper {
border: 1px solid @cp_forms-border;
border-radius: @variables_radius;
}
.CodeMirror {
border-radius: 0 0 @variables_radius @variables_radius;
}
}

View File

@ -1098,6 +1098,7 @@ define([
editor.setOption("extraKeys", {
"Esc": function () {
editor.display.input.blur();
$(cancelBlock).focus();
},
});
editor.setOption('lineNumbers', true);
@ -1115,7 +1116,12 @@ define([
}
editor.refresh();
editor.save();
editor.focus();
var firstBtn = $(block).find('.cp-markdown-toolbar').find('button').get(0);
if (firstBtn) {
firstBtn.focus();
} else {
editor.focus(); // fallback
}
});
if (APP.common && !(tmp && tmp.block) && cm) {
@ -1123,10 +1129,12 @@ define([
embed: function (mt) {
editor.focus();
editor.replaceSelection($(mt)[0].outerHTML);
}
},
toggleBar: true
});
var toggleRow = h('div.cp-markdown-toggle-row', markdownTb.toggleButton);
$(block).prepend(markdownTb.toolbar);
$(markdownTb.toolbar).show();
$(block).prepend(toggleRow);
cm.configureTheme(APP.common, function () {});
}
@ -1159,8 +1167,13 @@ define([
};
};
var outerWrapper = h('div.cp-editor-wrapper', [
toggleRow,
block
]);
return [
block,
outerWrapper,
cancelBlock
];
},
@ -1843,7 +1856,14 @@ define([
});
return res;
},
reset: function () { $(tag).find('input').removeAttr('checked'); },
reset: function () {
$(tag).find('input').each(function (i, input) {
var $i = $(input);
if (Util.isChecked($i)) {
$i.prop('checked', false);
}
});
},
setEditable: function (state) {
if (state) { $(tag).find('input').removeAttr('disabled'); }
else { $(tag).find('input').attr('disabled', 'disabled'); }
@ -1968,7 +1988,14 @@ define([
});
return res;
},
reset: function () { $(tag).find('input').removeAttr('checked'); },
reset: function () {
$(tag).find('input').each(function (i, input) {
var $i = $(input);
if (Util.isChecked($i)) {
$i.prop('checked', false);
}
});
},
setEditable: function (state) {
if (state) { $tag.find('input').removeAttr('disabled'); }
else { $tag.find('input').attr('disabled', 'disabled'); }
@ -2113,7 +2140,10 @@ define([
Object.keys(answers).forEach(function (author) {
var obj = answers[author];
var answer = Flatpickr.formatDate(new Date(obj.msg[uid]), dateFormat);
var answer;
if (obj.msg[uid]) {
answer = Flatpickr.formatDate(new Date(obj.msg[uid]), dateFormat);
}
if (isEmpty(answer)) { return empty++; }
results.push(h('div.cp-charts-row', h('span.cp-value', answer)));
});
@ -2185,7 +2215,12 @@ define([
return res;
},
reset: function () {
$(tag).find('input').removeAttr('checked');
$(tag).find('input').each(function (i, input) {
var $i = $(input);
if (Util.isChecked($i)) {
$i.prop('checked', false);
}
});
checkDisabled();
},
setEditable: function (state) {
@ -2328,8 +2363,13 @@ define([
return res;
},
reset: function () {
$(tag).find('input').removeAttr('checked');
lines.forEach(checkDisabled);
$(tag).find('input').each(function (i, input) {
var $i = $(input);
if (Util.isChecked($i)) {
$i.prop('checked', false);
}
});
},
setEditable: function (state) {
if (state) { lines.forEach(checkDisabled); }
@ -5558,30 +5598,43 @@ define([
]);
var editButtons = h('div.cp-form-edit-buttons-container', [ preview, edit, del ]);
var toggleRow = h('div.cp-markdown-toggle-row');
var markdownWrapper = h('div.cp-form-markdown-editor-wrapper', t);
var editDiv, previewDiv;
var div = h('div.cp-form-block.editable.nodrag.cp-form-submit-message', [
h('div.cp-form-block-content', [
p,
editDiv = h('div.cp-form-response-modal', t),
editDiv = h('div.cp-form-response-modal', [
toggleRow,
markdownWrapper
]),
previewDiv = h('div.cp-form-response-preview#cp-response-preview'),
editButtons
]),
]);
var cm = APP.responseCM = SFCodeMirror.create("gfm", CMeditor, t);
var editor = APP.responseEditor = cm.editor;
editor.setOption("extraKeys", {
"Esc": function() {
editor.display.input.blur();
$(preview).focus();
}
});
var markdownTb = APP.common.createMarkdownToolbar(editor, {
embed: function (mt) {
editor.focus();
editor.replaceSelection($(mt)[0].outerHTML);
}
},
toggleBar: true
});
var $tb = $(markdownTb.toolbar).insertAfter($(p));
$(toggleRow).append(markdownTb.toggleButton);
$(markdownWrapper).prepend(markdownTb.toolbar);
var $edit = $(editDiv);
var $preview = $(previewDiv);
var $p = $(preview);
var $e = $(edit);
var $tb = $(markdownTb);
var previewState = true;
var updatePreview = function () {
@ -5598,6 +5651,14 @@ define([
$preview.hide();
editor.refresh();
$tb.show();
setTimeout(function () {
var firstBtn = $(markdownTb.toolbar).find('button').get(0);
if (firstBtn) {
firstBtn.focus();
} else {
editor.focus(); // fallback
}
}, 0);
});
APP.$e = $e;
APP.$p = $p;

View File

@ -23,10 +23,14 @@ define([
var getPropChannels = function () {
return channels;
};
var addData = function (meta, CryptPad, user, Utils) {
var addData = function (meta, CryptPad, user, Utils, parsedHash) {
var keys = Utils.secret && Utils.secret.keys;
var parsed = Utils.Hash.parseTypeHash('pad', hash.slice(1));
var parsed;
if (parsedHash) {
parsed = parsedHash.hashData;
} else {
parsed = Utils.Hash.parseTypeHash('pad', hash.slice(1));
}
if (parsed && parsed.auditorKey) {
meta.form_auditorKey = parsed.auditorKey;
meta.form_auditorHash = hash;

View File

@ -240,6 +240,7 @@ define([
href: href,
initialState: blob,
config: {
readOnly: data.readOnly,
fileName: data.name,
fileType: data.ext,
autosave: data.autosave,

View File

@ -120,9 +120,31 @@
}
margin-bottom: 5px;
}
.cp-markdown-label-row {
margin-bottom: 0.5rem;
position: relative;
}
.cp-markdown-label-row label {
display: inline-block;
margin: 0;
vertical-align: middle;
}
.cp-markdown-toggle-button {
.markdown-toolbar-button-style();
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
&.cp-toolbar-button-active {
background-color: transparent;
}
}
#cp-kanban-edit-body {
border: 1px solid @cp_forms-border;
border-radius: @variables_radius;
overflow: hidden;
.CodeMirror {
height: 105px;
resize: vertical;
@ -132,7 +154,7 @@
box-sizing: content-box;
}
.cp-markdown-toolbar {
background-color: @cp_kanban-conflict-bg;
background-color: transparent;
color: @cp_kanban-fg;
border-radius: @variables_radius @variables_radius 0px 0px;
button {
@ -141,6 +163,20 @@
color: @cp_kanban-conflict-bg;
}
}
.btn {
outline: 0;
border-radius: @variables_radius;
.fa {
font: normal normal normal 14px/1 FontAwesome;
font-family: FontAwesome;
}
&:hover {
background-color: contrast(@cp_toolbar-bg, darken(@cp_toolbar-bg, 5%), lighten(@cp_toolbar-bg, 5%));
}
&:focus-visible {
outline: @variables_focus_style;
}
}
}
margin-bottom: 15px;
}
@ -241,9 +277,8 @@
background: @cp_kanban-item-bg;
box-shadow: @cryptpad_ui_shadow;
.tools_unselectable();
touch-action: none;
&:not(.no-drag) {
cursor: move;
touch-action: none;
cursor: grab;
}
@ -515,6 +550,9 @@
background-color: @cp_kanban-fg;
color: @cp_kanban-item-bg;
}
&:focus-visible {
outline: @variables_focus_style;
}
}
}
}

View File

@ -174,7 +174,6 @@ define([
}
_updateBoardsThrottle(framework, kanban, boards);
};
var editModal;
var PROPERTIES = ['title', 'body', 'tags', 'color'];
var BOARD_PROPERTIES = ['title', 'color'];
@ -200,6 +199,10 @@ define([
commit();
});
var markdownEditorWrapper = h('div.cp-markdown-label-row', [
h('label', { for: 'cp-kanban-edit-body' }, Messages.kanban_body)
]);
var conflicts, conflictContainer, titleInput, tagsDiv, text;
var content = h('div', [
conflictContainer = h('div#cp-kanban-edit-conflicts', [
@ -208,7 +211,7 @@ define([
]),
h('label', {for:'cp-kanban-edit-title'}, Messages.kanban_title),
titleInput = h('input#cp-kanban-edit-title'),
h('label', {for:'cp-kanban-edit-body'}, Messages.kanban_body),
markdownEditorWrapper,
h('div#cp-kanban-edit-body', [
text = h('textarea')
]),
@ -280,10 +283,21 @@ define([
embed: function (mt) {
editor.focus();
editor.replaceSelection($(mt)[0].outerHTML);
},
toggleBar: true
});
$(markdownEditorWrapper).append(markdownTb.toggleButton);
$(markdownTb.toolbar).on('keydown', function (e) {
if (e.which === 27) { // Escape key
e.preventDefault();
e.stopPropagation();
editor.focus(); // Focus the editor instead of closing the modal
}
else if (e.which === 13 || e.which === 9) { // "Enter" or "Tab" key should not close modal
e.stopPropagation();
}
});
$(text).before(markdownTb.toolbar);
$(markdownTb.toolbar).show();
editor.refresh();
var body = {
getValue: function () {
@ -1150,15 +1164,24 @@ define([
allTags.forEach(function (t) {
var tag;
$list.append(tag = h('span', {
'data-tag': t
'data-tag': t,
'tabindex': 0,
'role': 'button',
'aria-pressed': 'false'
}, t));
var $tag = $(tag).click(function () {
if ($tag.hasClass('active')) {
$tag.removeClass('active');
$tag.attr('aria-pressed', 'false');
} else {
$tag.addClass('active');
$tag.attr('aria-pressed', 'true');
}
commitTags();
}).keydown(function (e) {
if (e.which === 13 || e.which === 32) {
$tag.click();
}
});
});
};
@ -1186,7 +1209,7 @@ define([
commitTags();
});
let toggleTagsButton = h('button.btn.btn-toolbar-alt.cp-kanban-toggle-tags', [
let toggleTagsButton = h('button.btn.btn-toolbar-alt.cp-kanban-toggle-tags', {'aria-expanded': 'true'}, [
h('i.fa.fa-tags'),
h('span', Messages.fm_tagsName)
]);
@ -1198,6 +1221,7 @@ define([
let toggle = () => {
$tags.toggle();
let visible = $tags.is(':visible');
$toggleBtn.attr('aria-expanded', visible.toString());
$(toggleContainer).toggleClass('cp-kanban-container-flex', !visible);
$toggleBtn.toggleClass('btn-toolbar-alt', visible);
$toggleBtn.toggleClass('btn-toolbar', !visible);

View File

@ -662,6 +662,7 @@ define([
var customCss = '/customize/ckeditor-contents.css?' + window.CKEDITOR.CRYPTPAD_URLARGS;
$iframe.find('head').append('<link href="' + customCss + '" type="text/css" rel="stylesheet" _fcktemp="true"/>');
$('#cp-app-pad-editor').show();
framework._.sfCommon.addShortcuts(ifrWindow);
@ -1543,6 +1544,7 @@ define([
$iframe.find('html').css({
'background-color': '#FFF'
});
$('#cp-app-pad-editor').css('display', 'none');
}).nThen(waitFor());
}).nThen(function(waitFor) {

View File

@ -8,6 +8,7 @@
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
@import (reference) '../../customize/src/less2/include/variables.less';
@import (reference) '../../customize/src/less2/include/badges.less';
@import (reference) '../../customize/src/less2/include/avatar.less';
&.cp-app-profile {
@ -22,38 +23,19 @@
#cp-sidebarlayout-leftside {
display: none !important;
}
div#cp-sidebarlayout-container #cp-sidebarlayout-rightside {
padding: 1rem 2rem;
}
.cp-app-profile-section:first-child {
margin-top: 1rem;
}
.cp-profile-badges {
display: flex;
align-items: center;
& > span {
margin-right: 1em;
}
.cp-profile-badges-list {
.badges_main(40px);
i {
cursor: pointer;
&:focus-visible {
outline: @variables_focus_style;
}
&:not(:last-child) {
margin-right: 0.5em;
}
&.cp-selected {
border-color: @cryptpad_color_brand;
}
}
}
}
#cp-app-profile-header {
display: flex;
flex-flow: row wrap;
justify-content: center;
gap: 2rem;
#cp-app-profile-rightside {
flex: 1;
display: flex;
@ -63,20 +45,25 @@
}
}
#cp-app-profile-avatar {
margin: 10px;
margin-right: 20px;
text-align: center;
max-width: 300px;
max-height: 300px;
margin-bottom: 3.5rem;
margin-right: 0;
&> span {
display: inline-block;
.avatar_main(300px);
display: flex;
text-align: center;
border-radius: 4px;
overflow: hidden;
overflow: visible;
position: relative;
i.cp-badge {
border-width: 3px;
}
.cp-app-profile-avatar-delete {
right: 0;
top: 0;
position: absolute;
opacity: 1;
&:hover {
@ -90,6 +77,9 @@
vertical-align: top;
border-radius: @variables_radius_L;
}
.cp-avatar-default {
border-radius: @variables_radius_L;
}
media-tag {
height: 100%;
width: 100%;
@ -110,6 +100,12 @@
margin: 5px !important;
}
}
.cp-markdown-toggle-row {
margin-bottom: 0.5rem;
.btn.cp-markdown-toggle-button {
.markdown-toolbar-button-style() !important;
}
}
.cp-app-profile-resizer {
text-align: center;
@ -170,14 +166,13 @@
.cp-app-profile-link-code {
min-width: 0;
}
}
}
}
div.cp-app-profile-mute-container.cp-app-profile-section {
p {
margin-bottom: 0;
}
}
.cp-app-profile-friend {
display: flex;
@ -204,9 +199,9 @@
.cp-app-profile-description-code {
display: none;
.cp-markdown-toolbar {
display: flex;
height: auto;
flex-wrap: wrap;
max-width: 31rem;
background-color: @cp_toolbar-bg;
border-radius: @variables_radius @variables_radius 0 0;
}
}
#cp-app-profile-description-info {
@ -229,6 +224,7 @@
font-size: 16px;
line-height: initial;
max-width: 31rem;
border-radius: 0 0 @variables_radius @variables_radius;
pre {
margin: 0;
font-family: inherit;

View File

@ -22,15 +22,7 @@ define([
'/customize/messages.js',
'/customize/application_config.js',
'/components/marked/marked.min.js',
'/common/sframe-common-codemirror.js',
'cm/lib/codemirror',
'cm/mode/gfm/gfm',
'css!/components/codemirror/lib/codemirror.css',
'css!/components/codemirror/addon/dialog/dialog.css',
'css!/components/codemirror/addon/fold/foldgutter.css',
'css!/components/bootstrap/dist/css/bootstrap.min.css',
'css!/components/components-font-awesome/css/font-awesome.min.css',
'less!/profile/app-profile.less',
@ -53,10 +45,7 @@ define([
h,
Messages,
AppConfig,
Marked,
SFCodeMirror,
CodeMirror
)
Marked)
{
var APP = window.APP = {
_onRefresh: []
@ -94,7 +83,6 @@ define([
var LINK_ID = "cp-app-profile-link";
var AVATAR_ID = "cp-app-profile-avatar";
var DESCRIPTION_ID = "cp-app-profile-description";
var BADGES_ID = "cp-app-profile-badges";
var CREATE_ID = "cp-app-profile-create";
var HEADER_ID = "cp-app-profile-header";
var HEADER_RIGHT_ID = "cp-app-profile-rightside";
@ -105,13 +93,14 @@ define([
var sFrameChan;
var addViewButton = function ($container) {
if (APP.readOnly) {
if (!APP.isOwnProfile) {
return;
}
var hash = common.getMetadataMgr().getPrivateData().hashes.viewHash;
var url = APP.origin + '/profile/#' + hash;
/*
var $blockView = $('<div>', {class: PROFILE_SECTION}).appendTo($container);
var button = h('button.btn.' + VIEW_PROFILE_BUTTON, {
'aria-labelledby': 'cp-profile-view-button'
@ -121,7 +110,7 @@ define([
$(button).click(function () {
window.open(url, '_blank');
}).appendTo($blockView);
*/
var $blockShare = $('<div>', {class: PROFILE_SECTION}).appendTo($container);
var buttonS = h('button.btn.btn-primary.' + VIEW_PROFILE_BUTTON, {
'aria-labelledby': 'cp-profile-share-button'
@ -162,41 +151,6 @@ define([
});
APP.$linkEdit = $();
if (APP.readOnly) { return; }
var button = h('button.btn', {
title: Messages.clickToEdit
}, Messages.profile_addLink);
APP.$linkEdit = $(button);
$block.append(button);
var save = h('button.btn.btn-primary', { 'aria-labelledby': 'cp-save-link' }, Messages.settings_save);
var text = h('input#cp-save-link');
var code = h('div.cp-app-profile-link-code', [
text,
save
]);
var div = h('div.cp-app-profile-link-edit', [
code
]);
$block.append(div);
$(button).click(function () {
$(text).val(APP.$link.attr('href'));
$(code).css('display', 'flex');
APP.editor.refresh();
$(button).hide();
});
$(save).click(function () {
$(save).hide();
APP.module.execCommand('SET', {
key: 'url',
value: $(text).val()
}, function (data) {
APP.updateValues(data);
$(code).hide();
$(button).show();
$(save).show();
});
});
};
var refreshLink = function (data) {
APP.$linkEdit.removeClass('fa-pencil').removeClass('fa');
@ -360,7 +314,7 @@ define([
}, [
h('i.fa.fa-bell-slash', {'aria-hidden': 'true' }),
h('span#cp-profile-mute-button', Messages.contacts_mute || 'mute')
]);
]);
$(muteButton).click(function () {
module.execCommand('MUTE_USER', {
curvePublic: data.curvePublic,
@ -375,122 +329,49 @@ define([
});
};
var displayAvatar = function (val) {
var sframeChan = common.getSframeChannel();
var displayAvatar = function (val, data, badgeOK) {
var $span = APP.$avatar;
$span.empty();
if (!val) {
$('<img>', {
src: '/customize/images/avatar.png',
title: Messages.profile_defaultAlt,
alt: Messages.profile_defaultAlt,
}).appendTo($span);
const badge = data?.badge;
if (badge && !badgeOK) {
if (!data.proof || !data.edPublic) {
return displayAvatar(val);
}
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
APP.badge.execCommand('CHECK_BADGE', {
badge: badge,
//channel: privateData.channel,
ed: data.edPublic,
sig: data.proof,
nid: privateData.channel
}, res => {
if (!res?.verified) {
if (Badges.safeBadges.includes(data.badge)) {
delete data.badge;
} else {
data.badge = 'error';
}
displayAvatar(val, data, true);
return;
}
displayAvatar(val, data, true);
});
return;
}
common.displayAvatar($span, val);
if (APP.readOnly) { return; }
var $delButton = $('<button>', {
'class': 'cp-app-profile-avatar-delete btn btn-danger fa fa-times',
title: Messages.profile_remove_avatar
});
$span.append($delButton);
$delButton.click(function () {
var old = common.getMetadataMgr().getUserData().avatar;
APP.module.execCommand("SET", {
key: 'avatar',
value: ""
}, function () {
sframeChan.query("Q_PROFILE_AVATAR_REMOVE", old, function (err, err2) {
if (err || err2) { return void UI.log(err || err2); }
displayAvatar();
});
});
});
const name = data?.name || Messages.anonymous;
common.displayAvatar($span, val, name, void 0,
void 0, badge);
};
var addAvatar = function ($container) {
var $block = $('<div>', {id: AVATAR_ID}).appendTo($container);
APP.$avatar = $('<span>').appendTo($block);
var sframeChan = common.getSframeChannel();
APP.$avatar = $(h('span.cp-avatar')).appendTo($block);
displayAvatar();
if (APP.readOnly) { return; }
var data = MT.addAvatar(common, function (ev, data) {
var old = common.getMetadataMgr().getUserData().avatar;
var todo = function () {
APP.module.execCommand("SET", {
key: 'avatar',
value: data.url
}, function () {
sframeChan.query("Q_PROFILE_AVATAR_ADD", data.url, function (err, err2) {
if (err || err2) { return void UI.log(err || err2); }
displayAvatar(data.url);
});
});
};
if (old) {
sframeChan.query("Q_PROFILE_AVATAR_REMOVE", old, function (err, err2) {
if (err || err2) { return void UI.log(err || err2); }
todo();
});
return;
}
todo();
});
//upload profile photo button should be secondary
var $upButton = common.createButton('upload', false, data);
$upButton.removeClass('btn-primary').addClass('btn-secondary');
$upButton.removeProp('title');
$upButton.text(Messages.profile_upload);
$upButton.prepend($('<i>', {'class': 'fa fa-upload', 'aria-hidden': 'true'}));
$block.append($upButton);
};
var refreshAvatar = function (data) {
displayAvatar(data.avatar);
displayAvatar(data.avatar, data);
};
const addBadges = $container => {
var $block = $('<div>', {id: BADGES_ID, class:'cp-sidebarlayout-element'}).appendTo($container);
APP.$badges = $(h('span')).appendTo($block);
};
const refreshBadges = (obj) => {
if (!APP.$badges) { return; }
const metadataMgr = APP.common.getMetadataMgr();
const privateData = metadataMgr.getPrivateData();
let args = {};
if (!privateData.isOwnProfile) { args.edPublic = obj.edPublic; }
APP.badge.execCommand('LIST_BADGES', args, data => {
APP.$badges.empty();
let spinner;
APP.$badges.toggle(!!data.length);
let all = data.map(str => {
const i = Badges.render(str);
const $i = $(i).attr('tabindex', 0);
if (APP.readOnly) { return i; }
const selected = obj?.badge === str;
if (selected) { $i.addClass('cp-selected'); }
Util.onClickEnter($i, () => {
let value = selected ? '' : str;
spinner.spin();
APP.module.execCommand('SET', {
key: 'badge',
value
}, function (data) {
spinner.hide();
APP.updateValues(data);
});
});
return i;
});
let content = h('div.cp-profile-badges', [
h('span', Messages.profile_badges),
h('div.cp-profile-badges-list', all)
]);
APP.$badges.append(content);
spinner = UI.makeSpinner(APP.$badges.find('> div'));
});
};
var addDescription = function ($container) {
var $block = $('<div>', {id: DESCRIPTION_ID, class: PROFILE_SECTION}).appendTo($container);
@ -500,65 +381,6 @@ define([
}).appendTo($block);
APP.$descriptionEdit = $();
if (APP.readOnly) { return; }
var button = h('button.btn.btn-secondary', {
'aria-labelledby': 'cp-profile-add-description-button'
}, [
h('i.fa.fa-pencil', {'aria-hidden': 'true' }),
h('span#cp-profile-add-description-button', Messages.profile_addDescription)
]);
APP.$descriptionEdit = $(button);
var save = h('button.btn.btn-primary', Messages.settings_save);
var text = h('textarea');
var code = h('div.cp-app-profile-description-code', [
text,
h('br'),
save
]);
var div = h('div.cp-app-profile-description-edit', [
h('p.cp-app-profile-info', Messages.profile_info),
button,
code
]);
$block.append(div);
$(div).insertBefore(APP.$description);
var cm = SFCodeMirror.create("gfm", CodeMirror, text);
var editor = APP.editor = cm.editor;
editor.setOption('lineNumbers', true);
editor.setOption('lineWrapping', true);
editor.setOption('styleActiveLine', true);
editor.setOption('readOnly', false);
cm.configureTheme(common, function () {});
editor.setOption("extraKeys", {
"Esc": function () {
cm.getInputField().blur();
$(save).focus();
}
});
var markdownTb = common.createMarkdownToolbar(editor);
$(code).prepend(markdownTb.toolbar);
$(markdownTb.toolbar).show();
$(button).click(function () {
$(code).show();
APP.editor.refresh();
$(button).hide();
});
$(save).click(function () {
$(save).hide();
APP.module.execCommand('SET', {
key: 'description',
value: editor.getValue()
}, function (data) {
APP.updateValues(data);
$(code).hide();
$(button).show();
$(save).show();
});
});
};
var refreshDescription = function (data) {
var descriptionData = data.description || "";
@ -654,7 +476,6 @@ define([
addLink($rightside);
addFriendRequest($rightside);
addMuteButton($rightside);
//addBadges($rightside); // XXX 2025.6
addPublicKey($rightside);
addCopyData($rightside);
addViewButton($rightside);
@ -664,16 +485,14 @@ define([
}
};
var updateValues = APP.updateValues = function (data) {
var updateValues = APP.updateValues = function (_data) {
const data = Util.clone(_data);
// Only update avatar if it has changed
if (!APP._lastUpdate || APP._lastUpdate.avatar !== data.avatar) {
refreshAvatar(data);
}
refreshAvatar(data);
// Always update other profile information
refreshName(data);
refreshLink(data);
refreshDescription(data);
refreshBadges(data);
refreshFriendRequest(data);
refreshMute(data);
setPublicKeyButton(data);
@ -751,7 +570,7 @@ define([
onEvent: onEvent
});
if (privateData.isOwnProfile) {
APP.isOwnProfile = true;
APP.module = common.makeUniversal('profile', {
onEvent: onEvent
});
@ -759,7 +578,6 @@ define([
init();
console.log('POST SUBSCRIBE');
execCommand('SUBSCRIBE', null, function (obj) {
updateValues(obj);
UI.removeLoadingScreen();

View File

@ -18,40 +18,15 @@ define([
}).nThen(function (/*waitFor*/) {
var getSecrets = function (Cryptpad, Utils, cb) {
var Hash = Utils.Hash;
// 1st case: visiting someone else's profile with hash in the URL
// hash in the URL: visit someone else's profile
if (window.location.hash) {
// No password for profiles
return void cb(null, Hash.getSecrets('profile', window.location.hash.slice(1)));
const hash = window.location.hash.slice(1);
return cb(null, Hash.getSecrets('profile', hash));
}
nThen(function (waitFor) {
// 2nd case: visiting our own existing profile
Cryptpad.getProfileEditUrl(waitFor(function (hash) {
waitFor.abort();
return void cb(null, Hash.getSecrets('profile', hash));
}));
}).nThen(function () {
if (!Utils.LocalStore.isLoggedIn()) {
// Unregistered users can't create a profile
window.location.href = '/drive/';
return void cb();
}
// No password for profile
var hash = Hash.createRandomHash('profile');
var secret = Hash.getSecrets('profile', hash);
Cryptpad.pinPads([secret.channel], function (e) {
if (e) {
if (e === 'E_OVER_LIMIT') {
// TODO
}
return;
//return void UI.log(Messages._getKey('profile_error', [e])) // TODO
}
var profile = {};
profile.edit = Utils.Hash.getEditHashFromKeys(secret);
profile.view = Utils.Hash.getViewHashFromKeys(secret);
Cryptpad.setNewProfile(profile);
});
cb(null, secret);
// open our own profile
Cryptpad.getProfileViewUrl(function (hash) {
cb(null, Hash.getSecrets('profile', hash));
});
};
var addData = function (meta, Cryptpad, user) {
@ -59,21 +34,6 @@ define([
window.location.hash.slice(1) === user.profile;
};
var addRpc = function (sframeChan, Cryptpad, Utils) {
// Adding a new avatar from the profile: pin it and store it in the object
sframeChan.on('Q_PROFILE_AVATAR_ADD', function (data, cb) {
var chanId = Utils.Hash.hrefToHexChannelId(data, null);
Cryptpad.pinPads([chanId], function (e) {
if (e) { return void cb(e); }
Cryptpad.setAvatar(data, cb);
});
});
// Removing the avatar from the profile: unpin it
sframeChan.on('Q_PROFILE_AVATAR_REMOVE', function (data, cb) {
var chanId = Utils.Hash.hrefToHexChannelId(data, null);
Cryptpad.unpinPads([chanId], function () {
Cryptpad.setAvatar(undefined, cb);
});
});
sframeChan.on('EV_PROFILE_CORRUPTED_CACHE', function () {
Utils.Cache.clearChannel(Utils.secret.channel, function () {

View File

@ -15,10 +15,11 @@ define([
'/common/common-feedback.js',
'/common/outer/local-store.js',
'/common/hyperscript.js',
'/common/extensions.js',
'/customize/pages.js',
'css!/components/components-font-awesome/css/font-awesome.min.css',
], function (Config, $, Login, Cryptpad, Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h, Pages) {
], function (Config, $, Login, Cryptpad, Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h, Extensions, Pages) {
if (window.top !== window) { return; }
var Messages = Cryptpad.Messages;
$(function () {
@ -146,6 +147,30 @@ define([
return void UI.alert(Messages.register_mustAcceptTerms);
}
const extensions = [];
Extensions.getExtensionsSync('POST_REGISTER').forEach(ext => {
try {
extensions.push(ext);
} catch (error) {
console.error(error);
}
});
const cb = (data) => {
// data.edPublic, data.edPrivate
const next = Util.mkAsync(() => {
Login.redirect();
});
if (!extensions.length) {
next();
} else {
extensions.forEach(ext => {
const content = ext.getContent(data);
UI.emptyLoadingScreen(content);
});
}
return true;
};
setTimeout(function () {
var span = h('span', [
h('h2', [
@ -156,8 +181,7 @@ define([
Messages.register_warning_note
]);
UI.confirm(span,
function (yes) {
UI.confirm(span, function (yes) {
if (!yes) { return; }
Login.loginOrRegisterUI({
@ -166,7 +190,8 @@ define([
token,
isRegister: true,
shouldImport,
onOTP: UI.getOTPScreen
onOTP: UI.getOTPScreen,
cb
});
}, {
ok: Messages.register_writtenPassword,

View File

@ -9,6 +9,9 @@
@import (reference) "../../customize/src/less2/include/creation.less";
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/export.less';
@import (reference) '../../customize/src/less2/include/badges.less';
@import (reference) '../../customize/src/less2/include/avatar.less';
@import (reference) "../../customize/src/less2/include/markdown.less";
&.cp-app-settings {
.framework_min_main();
@ -229,6 +232,112 @@
margin: 0 2px 0 0;
}
}
.cp-settings-badges {
display: flex;
flex-flow: column;
align-items: flex-start;
.cp-settings-badges-list {
display: flex;
align-items: center;
font-size: 24px;
margin-top: 10px;
.badges_main(40px);
i {
cursor: pointer;
&:focus-visible {
outline: @variables_focus_style;
}
&:not(:last-child) {
margin-right: 0.5em;
}
}
}
}
.cp-settings-profile-avatar {
.cp-settings-avatar-container {
width: 300px;
margin-bottom: 1.5rem;
border-radius: 10px;
}
.cp-avatar {
box-sizing: content-box;
.avatar_main(300px);
display: flex;
text-align: center;
border-radius: 4px;
overflow: visible;
position: relative;
i.cp-badge {
border-width: 3px;
}
.cp-settings-avatar-delete {
right: 0;
top: 0;
position: absolute;
opacity: 1;
&:hover {
opacity: 0.7;
}
}
}
img {
max-width: 100%;
max-height: 100%;
vertical-align: top;
border-radius: @variables_radius_L;
}
.cp-avatar-default {
border-radius: @variables_radius_L;
}
media-tag {
height: 100%;
width: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
img {
min-width: 100%;
min-height: 100%;
max-width: none;
max-height: none;
flex: 1;
}
}
button {
min-width: 40px;
height: 40px;
margin: 5px !important;
}
}
.cp-settings-profile-description {
width: 40rem;
max-width: 100%;
.cp-labelled-input {
max-width: 100% !important;
width: 100% !important;
}
textarea, .CodeMirror {
height: 175px;
border-radius: 10px;
}
.CodeMirror {
font-family: monospace;
font-size: 16px;
line-height: initial;
max-width: 40rem;
border: 1px solid fade(@cryptpad_text_col, 50%);
margin: 5px 0;
pre {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
}
.cp-markdown-toggle-button {
.markdown-toolbar-button-style();
}
}
}
}

View File

@ -11,6 +11,9 @@ define([
'/common/common-ui-elements.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/inner/sidebar-layout.js',
'/common/inner/badges.js',
'/common/inner/common-mediatag.js',
'/customize/messages.js',
'/common/hyperscript.js',
'/common/common-credential.js',
@ -21,8 +24,16 @@ define([
'/common/common-constants.js',
'/customize.dist/login.js',
'/common/sframe-common-codemirror.js',
'cm/lib/codemirror',
'cm/mode/gfm/gfm',
'/common/jscolor.js',
'/components/file-saver/FileSaver.min.js',
'css!/components/codemirror/lib/codemirror.css',
'css!/components/codemirror/addon/dialog/dialog.css',
'css!/components/codemirror/addon/fold/foldgutter.css',
'css!/components/bootstrap/dist/css/bootstrap.min.css',
'css!/components/components-font-awesome/css/font-awesome.min.css',
'less!/settings/app-settings.less',
@ -35,6 +46,9 @@ define([
UIElements,
Util,
Hash,
Sidebar,
Badges,
MT,
Messages,
h,
Cred,
@ -43,7 +57,9 @@ define([
Backup,
Feedback,
Constants,
Login
Login,
SFCodeMirror,
CodeMirror
) {
var saveAs = window.saveAs;
var APP = window.APP = {};
@ -52,17 +68,24 @@ define([
var metadataMgr;
var privateData;
var sframeChan;
var onProfileEvt = Util.mkEvent();
var categories = {
'account': [ // Msg.settings_cat_account
'cp-settings-own-drive',
'cp-settings-info-block',
'cp-settings-displayname',
'cp-settings-language-selector',
'cp-settings-mediatag-size',
'cp-settings-delete'
],
'profile': [ // Msg.settings_cat_profile
'cp-settings-profile-header',
'cp-settings-profile-name',
'cp-settings-profile-avatar',
'cp-settings-profile-badges',
'cp-settings-profile-link',
'cp-settings-profile-description',
],
'security': [ // Msg.settings_cat_security
'cp-settings-logout-everywhere',
'cp-settings-mfa',
@ -110,26 +133,15 @@ define([
],
'notifications': [
'cp-settings-notif-calendar'
],
'subscription': {
onClick: function() {
var urls = common.getMetadataMgr().getPrivateData().accounts;
window.open(urls.upgradeURL);
Feedback.send('SUBSCRIPTION_BUTTON');
}
}
]
};
if (AppConfig.disableFeedback) {
var feedbackIdx = categories.account.indexOf('cp-settings-userfeedback');
categories.account.splice(feedbackIdx, 1);
var feedbackIdx = categories.security.indexOf('cp-settings-userfeedback');
categories.security.splice(feedbackIdx, 1);
}
if (AppConfig.disableProfile) {
var displaynameIdx = categories.account.indexOf('cp-settings-displayname');
categories.account.splice(displaynameIdx, 1);
}
if (!ApiConfig.allowSubscriptions) {
delete categories.subscription;
delete categories.profile;
}
var create = {};
@ -146,11 +158,14 @@ define([
.text(Messages['settings_' + safeKey + 'Hint'] || 'Coming soon...');
};
var makeBlock = function(key, getter, full) {
var makeBlock = function(key, getter, full, isNew) {
var safeKey = key.replace(/-([a-z])/g, function(g) { return g[1].toUpperCase(); });
create[key] = function() {
var $div = $('<div>', { 'class': 'cp-settings-' + key + ' cp-sidebarlayout-element' });
if (isNew) {
$div.attr('data-item', key);
}
if (full) {
$('<label>').text(Messages['settings_' + safeKey + 'Title'] || key).appendTo($div);
@ -204,51 +219,6 @@ define([
return $div;
};
// Create the block containing the display name field
create['displayname'] = function() {
var $div = $('<div>', { 'class': 'cp-settings-displayname cp-sidebarlayout-element' });
$('<label>', { 'for': 'cp-settings-displayname' }).text(Messages.user_displayName).appendTo($div);
var $inputBlock = $('<div>', { 'class': 'cp-sidebarlayout-input-block' }).appendTo($div);
var $input = $('<input>', {
'type': 'text',
'id': 'cp-settings-displayname',
'placeholder': Messages.anonymous
}).appendTo($inputBlock);
var $save = $('<button>', { 'class': 'btn btn-primary' }).text(Messages.settings_save).appendTo($inputBlock);
var $ok = $('<span>', { 'class': 'fa fa-check', title: Messages.saved }).hide().appendTo($div);
var $spinner = $('<span>', { 'class': 'fa fa-spinner fa-pulse' }).hide().appendTo($div);
var displayName = metadataMgr.getUserData().name || '';
$input.val(displayName);
// When the display name is changed (enter or button clicked)
var todo = function() {
displayName = $input.val();
if (displayName === metadataMgr.getUserData().name) { return; }
$spinner.show();
common.setDisplayName(displayName, function() {
$spinner.hide();
$ok.show();
});
};
$input.on('keyup', function(e) {
if ($input.val() !== displayName) { $ok.hide(); }
if (e.which === 13) { todo(); }
});
$save.click(todo);
// On remote change
var onChange = function() {
if (metadataMgr.getUserData().name !== $input.val()) {
$input.val(metadataMgr.getUserData().name);
$input.focusout();
}
};
metadataMgr.onChange(onChange);
return $div;
};
create['language-selector'] = function() {
var $div = $('<div>', { 'class': 'cp-settings-language-selector cp-sidebarlayout-element' });
$('<label>').text(Messages.language).appendTo($div);
@ -546,31 +516,19 @@ define([
}, function() {
nThen(function (waitFor) {
$button.prop('disabled', 'disabled');
var priv = metadataMgr.getPrivateData();
// Check if subscriptions are enabled and you have a premium plan
if (priv.plan && priv.plan !== "custom" && ApiConfig.allowSubscriptions) {
// Also make sure upgradeURL is defined
var url = priv.accounts && priv.accounts.upgradeURL;
if (!url) { return; }
url += '#mysubs';
var a = h('a', { href:url }, Messages.settings_deleteSubscription);
$(a).click(function (e) {
e.preventDefault();
common.openUnsafeURL(url);
});
UI.confirm(h('div', [
Messages.settings_deleteWarning, h('p', a)
]), waitFor(function (yes) {
if (!yes) {
$button.prop('disabled', '');
waitFor.abort();
}
}), {
ok: Messages.settings_deleteContinue,
okClass: 'btn.btn-danger',
cancelClass: 'btn.btn-primary'
});
}
// Accounts plugin:
// Msg.settings_deleteSubscription
// Msg.settings_deleteWarning
// Msg.settings_deleteContinue
common.getExtensionsSync('ACCOUNT_DELETION').forEach(ext => {
if (!ext?.checkDeletion) {
return console.error('Invalid extension point', 'ACCOUNT_DELETION', ext);
}
ext.checkDeletion(common, $button, waitFor(allowed => {
if (allowed) { return; }
waitFor.abort();
}));
});
}).nThen(function () {
var password = $form.find('#cp-settings-delete-account').val();
if (!password) {
@ -1876,6 +1834,267 @@ define([
cb($cbox[0]);
}, true);
// Profile
makeBlock('profile-header', function(cb) {
cb();
}, true);
makeBlock('profile-name', function(cb) {
const input = APP.blocks.input();
const button = APP.blocks.button('primary', '',
Messages.settings_save);
const inputButton = APP.blocks.inputButton(input, button, {
onEnterDelegate: true
});
const labelled = APP.blocks.labelledInput(
Messages.user_displayName, input, inputButton);
let displayName = metadataMgr.getUserData().name || '';
const $input = $(input).val(displayName || '');
Util.onClickEnter($(button), () => {
const value = $(input).val();
if (value === displayName) { return; }
common.setDisplayName(value, function() {
displayName = value;
UI.log(Messages.saved);
});
});
onProfileEvt.reg(() => {
$input.val(APP.profileData?.name || '');
});
cb(labelled);
}, false, true);
makeBlock('profile-link', function(cb) {
if (!common.isLoggedIn()) { return cb(false); }
const input = APP.blocks.input();
const button = APP.blocks.button('primary', '',
Messages.settings_save);
const inputButton = APP.blocks.inputButton(input, button, {
onEnterDelegate: true
});
const labelled = APP.blocks.labelledInput(
Messages.profile_addLink, input, inputButton);
const $input = $(input).val(APP.profileData?.url || '');
Util.onClickEnter($(button), () => {
const value = $(input).val();
APP.profile.execCommand('SET', {
key: 'url',
value
}, function (data) {
UI.log(Messages.saved);
APP.profileData = data;
onProfileEvt.fire();
});
});
onProfileEvt.reg(() => {
$input.val(APP.profileData?.url || '');
});
cb(labelled);
}, false, true);
const redrawBadges = ($badges) => {
const old = APP.profileData;
APP.badge.execCommand('LIST_BADGES', {}, data => {
let spinner;
$badges.toggle(!!data.length);
let all = data.map(str => {
const i = Badges.render(str);
const $i = $(i).attr('tabindex', 0);
const selected = old?.badge === str;
if (selected) { $i.addClass('cp-selected'); }
Util.onClickEnter($i, () => {
let value = selected ? '' : str;
spinner.spin();
APP.profile.execCommand('SET', {
key: 'badge',
value
}, function (data) {
APP.profileData = data;
spinner.done();
onProfileEvt.fire();
});
});
return i;
});
if (!all.length) {
return $badges.empty();
}
let content = h('div.cp-settings-badges', [
h('span', Messages.profile_badges),
h('div.cp-settings-badges-list', all)
]);
$badges.empty().append(content);
spinner = UI.makeSpinner($badges.find('.cp-settings-badges-list'));
});
};
makeBlock('profile-badges', function(cb) {
if (!common.isLoggedIn()) { return cb(false); }
const badges = h('div');
const $badges = $(badges);
redrawBadges($badges);
onProfileEvt.reg(() => {
redrawBadges($badges);
});
cb(badges);
}, false, true);
const redrawAvatar = ($avatar) => {
const val = APP.profileData?.avatar;
const badge = APP.profileData?.badge;
const name = APP.profileData?.name || Messages.anonymous;
if (!val) { $avatar.empty(); }
common.displayAvatar($avatar, val, name, () => {
if (!val) { return; }
// avatar cb: append delete button
$avatar.find('.cp-settings-avatar-delete').remove();
const delButton = h('button.cp-settings-avatar-delete.btn.btn-danger.fa.fa-times', {
'aria-label': Messages.profile_remove_avatar,
title: Messages.profile_remove_avatar
});
$avatar.append(delButton);
$(delButton).click(() => {
const old = APP.profileData?.avatar;
APP.profile.execCommand("SET", {
key: 'avatar',
value: ""
}, (newData) => {
APP.profileData = newData;
sframeChan.query("Q_PROFILE_AVATAR_REMOVE", old,
(err, err2) => {
if (err || err2) {
return void UI.warn(err || err2);
}
onProfileEvt.fire();
});
});
});
}, void 0, badge);
};
makeBlock('profile-avatar', function(cb) {
if (!common.isLoggedIn()) { return cb(false); }
const avatar = h('div.cp-avatar');
const $avatar = $(avatar);
redrawAvatar($avatar);
onProfileEvt.reg(() => {
redrawAvatar($avatar);
});
// Upload
const data = MT.addAvatar(common, (ev, data) => {
const old = APP.profileData?.avatar;
const todo = () => {
APP.profile.execCommand("SET", {
key: 'avatar',
value: data.url
}, (newData) => {
console.error(newData?.avatar, newData);
sframeChan.query("Q_PROFILE_AVATAR_ADD",
data.url, (err, err2) => {
if (err || err2) {
return void UI.warn(err || err2);
}
APP.profileData = newData;
onProfileEvt.fire();
});
});
};
if (old) {
sframeChan.query("Q_PROFILE_AVATAR_REMOVE",
old, (err, err2) => {
if (err || err2) {
return void UI.warn(err || err2);
}
todo();
});
return;
}
todo();
});
const $upButton = common.createButton('upload', false, data);
$upButton.removeClass('btn-primary').addClass('btn-secondary');
$upButton.removeProp('title');
$upButton.text(Messages.profile_upload);
$upButton.prepend($('<i>', {'class': 'fa fa-upload', 'aria-hidden': 'true'}));
cb([
h('label', Messages.settings_profileAvatarLabel),
h('div.cp-settings-avatar-container', avatar),
$upButton[0]
]);
}, false, true);
makeBlock('profile-description', function(cb) {
if (!common.isLoggedIn()) { return cb(false); }
const input = APP.blocks.textarea();
const button = APP.blocks.button('primary', '',
Messages.settings_save);
const labelled = APP.blocks.labelledInput(
Messages.profile_editDescription, input);
$(labelled).append(button);
$(input).val(APP.profileData?.description || '');
const cm = SFCodeMirror.create("gfm", CodeMirror, input);
const editor = APP.editor = cm.editor;
editor.setOption('lineNumbers', true);
editor.setOption('lineWrapping', true);
editor.setOption('styleActiveLine', true);
editor.setOption('readOnly', false);
cm.configureTheme(common, function () {});
editor.setOption("extraKeys", {
"Esc": function () {
$(button).focus();
}
});
editor.refresh();
Util.onClickEnter($(button), () => {
const value = editor.getValue();
APP.profile.execCommand('SET', {
key: 'description',
value
}, function (data) {
UI.log(Messages.saved);
APP.profileData = data;
onProfileEvt.fire();
});
});
const markdownTb = common.createMarkdownToolbar(editor, {
embed: function (mt) {
editor.focus();
editor.replaceSelection($(mt)[0].outerHTML);
},
toggleBar: true
});
$(input).before(markdownTb.toggleButton);
$(input).before(markdownTb.toolbar);
onProfileEvt.reg(() => {
editor.setValue(APP.profileData?.description || '');
editor.save();
editor.refresh();
});
cb(labelled);
}, false, true);
// Settings app
var createUsageButton = function() {
@ -1897,18 +2116,20 @@ define([
var SIDEBAR_ICONS = {
account: 'fa fa-user-o',
profile: 'fa fa-user-circle',
drive: 'fa fa-hdd-o',
cursor: 'fa fa-i-cursor',
code: 'fa fa-file-code-o',
pad: 'cptools cptools-richtext',
security: 'fa fa-lock',
subscription: 'fa fa-star-o',
kanban: 'cptools cptools-kanban',
style: 'cptools cptools-palette',
notifications: 'fa fa-bell'
};
let SIDEBAR_NAMES = {}; // for extension points
Messages.settings_cat_notifications = Messages.notificationsPage;
Messages.settings_cat_profile = Messages.profileButton;
var createLeftside = function() {
var $categories = $('<div>', { 'class': 'cp-sidebarlayout-categories' })
.appendTo(APP.$leftside);
@ -1924,12 +2145,14 @@ define([
});
}
let name = SIDEBAR_NAMES[key] ||
Messages['settings_cat_' + key] || key;
var $category = $(h('div.cp-sidebarlayout-category', {
'tabindex': 0,
'data-category': key
}, [
icon,
Messages['settings_cat_' + key] || key,
name,
])).appendTo($categories);
@ -1955,7 +2178,15 @@ define([
common.setHash(active);
};
var onProfileEvent = function (obj) {
var ev = obj.ev;
var data = obj.data;
if (ev === 'UPDATE') {
APP.profileData = data;
onProfileEvt.fire();
return;
}
};
nThen(function(waitFor) {
$(waitFor(UI.addLoadingScreen));
@ -1967,6 +2198,16 @@ define([
APP.$rightside = $('<div>', { id: 'cp-sidebarlayout-rightside' }).appendTo(APP.$container);
sframeChan = common.getSframeChannel();
sframeChan.onReady(waitFor());
}).nThen(function(waitFor) {
APP.profile = common.makeUniversal('profile', {
onEvent: onProfileEvent
});
APP.badge = common.makeUniversal('badge', {
onEvent: onProfileEvent
});
APP.profile.execCommand('SUBSCRIBE', null, waitFor(obj => {
APP.profileData = obj;
}));
}).nThen(function( /*waitFor*/ ) {
metadataMgr = common.getMetadataMgr();
privateData = metadataMgr.getPrivateData();
@ -1985,6 +2226,21 @@ define([
APP.toolbar.$rightside.hide();
APP.history = common.makeUniversal('history');
// EXTENSION_POINT:SETTINGS_CATEGORY
common.getExtensionsSync('SETTINGS_CATEGORY').forEach(ext => {
if (!ext || !ext.id || !ext.name || !ext.getContent) {
return console.error('Invalid extension point', 'SETTINGS_CATEGORY', ext);
}
if (categories[ext.id]) {
return console.error('Extension point ID already used', ext);
}
SIDEBAR_ICONS[ext.id] = ext.icon;
SIDEBAR_NAMES[ext.id] = ext.name;
categories[ext.id] = ext.getContent(common);
});
APP.blocks = Sidebar.blocks('settings', common);
// Content
var $rightside = APP.$rightside;
/*for (var f in create) {

View File

@ -108,6 +108,23 @@ define([
sframeChan.on('Q_SET_DRIVE_REDIRECT_PREFERENCE', function (data, cb) {
Cryptpad.setDriveRedirectPreference(data, cb);
});
// Adding a new avatar from the profile: pin it
// and store it in the profile and user objects
sframeChan.on('Q_PROFILE_AVATAR_ADD', function (data, cb) {
var chanId = Utils.Hash.hrefToHexChannelId(data, null);
Cryptpad.pinPads([chanId], function (e) {
if (e) { return void cb(e); }
Cryptpad.setAvatar(data, cb);
});
});
// Removing the avatar from the profile: unpin it
sframeChan.on('Q_PROFILE_AVATAR_REMOVE', function (data, cb) {
var chanId = Utils.Hash.hrefToHexChannelId(data, null);
Cryptpad.unpinPads([chanId], function () {
Cryptpad.setAvatar(undefined, cb);
});
});
};
var category;
if (window.location.hash) {

View File

@ -470,6 +470,7 @@ define([
var privateData = common.getMetadataMgr().getPrivateData();
var $contentContainer = $('#cp-app-slide-editor');
$contentContainer.show();
var $modal = $('#cp-app-slide-modal');
var $content = $('#cp-app-slide-modal-content');
var $print = $('#cp-app-slide-print');
@ -629,6 +630,7 @@ define([
CodeMirror = SFCodeMirror.create(null, CMeditor);
$('.CodeMirror').addClass('fullPage');
editor = CodeMirror.editor;
$('#cp-app-slide-editor').hide();
}).nThen(waitFor());
}).nThen(function (/*waitFor*/) {

View File

@ -208,14 +208,21 @@ define([
var addEvent = function () {
console.log($modal);
var icon_to;
$modal.mousemove(function () {
var mousemove = function () {
var $buttons = $modal.find('.cp-app-slide-modal-button');
$buttons.show();
if (icon_to) { window.clearTimeout(icon_to); }
icon_to = window.setTimeout(function() {
$buttons.fadeOut();
}, 1000);
});
};
$modal.mousemove(mousemove);
$modal.on('touchstart', mousemove);
$modal.on('touchend', mousemove);
$modal.on('touchcancel', mousemove);
$modal.on('touchmove', mousemove);
$modal.find('#cp-app-slide-modal-exit').click(function () {
var ev = $.Event("keyup");
ev.which = 27;

View File

@ -209,27 +209,14 @@ define([
};
create['subscribe'] = function () {
if (!Pages.areSubscriptionsAllowed()) { return; }
try {
if (common.getMetadataMgr().getPrivateData().plan) { return; }
} catch (err) {}
var url = Pages.accounts.upgradeURL;
var accountsLink = h('a', {
href: url,
}, Messages.support_premiumLink);
$(accountsLink).click(function (ev) {
ev.preventDefault();
common.openURL(url);
let content;
// Msg.support_premiumLink
// Msg.support_premiumPriority,
common.getExtensionsSync('SUPPORT_SUBSCRIBE').forEach(ext => {
if (!ext.getContent) { return; }
content = ext.getContent(common);
});
return $(h('div.cp-support-subscribe.cp-sidebarlayout-element', [
h('div.alert.alert-info', [
Messages.support_premiumPriority,
' ',
accountsLink,
]),
]));
return $(content);
};
// Create a new tickets

Some files were not shown because too many files have changed in this diff Show More