diff --git a/apps/common/mobile/lib/view/collaboration/Comments.jsx b/apps/common/mobile/lib/view/collaboration/Comments.jsx index 401f779563..92ee79bb8e 100644 --- a/apps/common/mobile/lib/view/collaboration/Comments.jsx +++ b/apps/common/mobile/lib/view/collaboration/Comments.jsx @@ -701,6 +701,9 @@ const ViewComments = inject("storeComments", "storeAppOptions", "storeReview")(o const comments = storeComments.groupCollectionFilter || storeComments.collectionComments; const isEdit = storeAppOptions.isEdit || storeAppOptions.isRestrictedEdit; const sortComments = comments.length > 0 ? [...comments].sort((a, b) => a.time > b.time ? -1 : 1) : null; + const isProtected = storeAppOptions.isProtected; + const typeProtection = storeAppOptions.typeProtection; + const isAvailableCommenting = !isProtected || typeProtection === Asc.c_oAscEDocProtect.TrackedChanges || typeProtection === Asc.c_oAscEDocProtect.Comments; const [clickComment, setComment] = useState(); const [commentActionsOpened, openActionComment] = useState(false); @@ -737,8 +740,8 @@ const ViewComments = inject("storeComments", "storeAppOptions", "storeReview")(o {isEdit && !viewMode &&
- {(comment.editable && displayMode === 'markup' && !wsProps?.Objects && (!isViewer || canEditComments)) &&
{onResolveComment(comment);}}>
} - {(displayMode === 'markup' && !wsProps?.Objects && (!isViewer || canEditComments)) && + {(comment.editable && displayMode === 'markup' && !wsProps?.Objects && (!isViewer || canEditComments) && isAvailableCommenting) &&
{onResolveComment(comment);}}>
} + {(displayMode === 'markup' && !wsProps?.Objects && (!isViewer || canEditComments) && isAvailableCommenting) &&
{setComment(comment); openActionComment(true);}}> @@ -768,7 +771,7 @@ const ViewComments = inject("storeComments", "storeAppOptions", "storeReview")(o
{reply.date}
- {isEdit && !viewMode && reply.editable && (!isViewer || canEditComments) && + {isEdit && !viewMode && reply.editable && (!isViewer || canEditComments) && isAvailableCommenting &&
{setComment(comment); setReply(reply); openActionReply(true);}} @@ -812,6 +815,9 @@ const CommentList = inject("storeComments", "storeAppOptions", "storeReview")(ob const viewMode = !storeAppOptions.canComments; const isEdit = storeAppOptions.isEdit || storeAppOptions.isRestrictedEdit; const comments = storeComments.showComments; + const isProtected = storeAppOptions.isProtected; + const typeProtection = storeAppOptions.typeProtection; + const isAvailableCommenting = !isProtected || typeProtection === Asc.c_oAscEDocProtect.TrackedChanges || typeProtection === Asc.c_oAscEDocProtect.Comments; const [currentIndex, setCurrentIndex] = useState(0); const comment = comments[currentIndex]; @@ -848,7 +854,7 @@ const CommentList = inject("storeComments", "storeAppOptions", "storeReview")(ob {isEdit && !viewMode && - {onCommentMenuClick('addReply', comment);}}>{_t.textAddReply} + {onCommentMenuClick('addReply', comment);}}>{_t.textAddReply} } {comments.length > 1 &&
@@ -870,15 +876,15 @@ const CommentList = inject("storeComments", "storeAppOptions", "storeReview")(ob
{isEdit && !viewMode && -
- {(comment.editable && displayMode === 'markup' && !wsProps?.Objects && (!isViewer || canEditComments)) &&
{onResolveComment(comment);}}>
} - {(displayMode === 'markup' && !wsProps?.Objects && (!isViewer || canEditComments)) && -
{openActionComment(true);}}> - -
- } -
+
+ {(comment.editable && displayMode === 'markup' && !wsProps?.Objects && (!isViewer || canEditComments) && isAvailableCommenting) &&
{onResolveComment(comment);}}>
} + {(displayMode === 'markup' && !wsProps?.Objects && (!isViewer || canEditComments) && isAvailableCommenting) && +
{openActionComment(true);}}> + +
+ } +
}
@@ -902,7 +908,7 @@ const CommentList = inject("storeComments", "storeAppOptions", "storeReview")(ob
{reply.date}
- {isEdit && !viewMode && reply.editable && (!isViewer || canEditComments) && + {isEdit && !viewMode && reply.editable && (!isViewer || canEditComments) && isAvailableCommenting &&
{setReply(reply); openActionReply(true);}} diff --git a/apps/common/mobile/resources/less/comments.less b/apps/common/mobile/resources/less/comments.less index d74217ad02..38aa914e0e 100644 --- a/apps/common/mobile/resources/less/comments.less +++ b/apps/common/mobile/resources/less/comments.less @@ -233,7 +233,7 @@ overflow: auto; .item-content .item-input-wrap::after { - background-color: @text-normal; + background-color: @brandColor; } } diff --git a/apps/common/mobile/resources/less/common-ios.less b/apps/common/mobile/resources/less/common-ios.less index e4ffca696e..2c5456ce85 100644 --- a/apps/common/mobile/resources/less/common-ios.less +++ b/apps/common/mobile/resources/less/common-ios.less @@ -55,6 +55,8 @@ --f7-picker-item-text-color: rgba(var(--text-normal), 0.45); --f7-picker-item-selected-text-color: @text-normal; + --f7-block-text-color: @text-secondary; + // Main Toolbar #editor-navbar.navbar .right a + a, #editor-navbar.navbar .left a + a { diff --git a/apps/common/mobile/resources/less/common-material.less b/apps/common/mobile/resources/less/common-material.less index 570d532854..61fd561d7b 100644 --- a/apps/common/mobile/resources/less/common-material.less +++ b/apps/common/mobile/resources/less/common-material.less @@ -50,6 +50,8 @@ --f7-input-bg-color: @background-primary; --f7-input-placeholder-color: @text-secondary; --f7-input-text-color: @text-normal; + + --f7-block-text-color: @text-secondary; .button { --f7-touch-ripple-color: transparent; diff --git a/apps/common/mobile/resources/less/common.less b/apps/common/mobile/resources/less/common.less index 0262372d5b..96b3a7a5a4 100644 --- a/apps/common/mobile/resources/less/common.less +++ b/apps/common/mobile/resources/less/common.less @@ -1115,7 +1115,31 @@ input[type="number"]::-webkit-inner-spin-button { } } - +// Block +.block { + margin-top: 8px; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; +} + +// Inputs List +.block-title + .list, .block-title + .block, .block-title + .card, .block-title + .timeline, .block-title + .block-header { + margin: 0; +} + +.inputs-list { + margin-bottom: 0; + + ul { + list-style: none; + margin: 0; + padding: 0; + position: relative; + background: var(--f7-list-bg-color); + } +} diff --git a/apps/common/mobile/resources/less/material/icons.less b/apps/common/mobile/resources/less/material/icons.less index 8147d58767..9ad5d918a4 100644 --- a/apps/common/mobile/resources/less/material/icons.less +++ b/apps/common/mobile/resources/less/material/icons.less @@ -26,6 +26,16 @@ display: none; } } + &.icon-show-password { + width: 24px; + height: 24px; + .encoded-svg-mask('', @text-secondary); + } + &.icon-hide-password { + width: 24px; + height: 24px; + .encoded-svg-mask('', @text-secondary); + } } .navbar { i.icon { diff --git a/apps/documenteditor/mobile/locale/en.json b/apps/documenteditor/mobile/locale/en.json index 8692f9da64..1bf4ffe3bf 100644 --- a/apps/documenteditor/mobile/locale/en.json +++ b/apps/documenteditor/mobile/locale/en.json @@ -427,7 +427,8 @@ "unknownErrorText": "Unknown error.", "uploadImageExtMessage": "Unknown image format.", "uploadImageFileCountMessage": "No images uploaded.", - "uploadImageSizeMessage": "The image is too big. The maximum size is 25 MB." + "uploadImageSizeMessage": "The image is too big. The maximum size is 25 MB.", + "errorPasswordIsNotCorrect": "The password you supplied is not correct. Verify that the CAPS LOCK key is off and be sure to use the correct capitalization." }, "LongActions": { "applyChangesTextText": "Loading data...", @@ -476,6 +477,11 @@ "errorUpdateVersion": "The file version has been changed. The page will be reloaded.", "leavePageText": "You have unsaved changes. Click 'Stay on this Page' to wait for autosave. Click 'Leave this Page' to discard all the unsaved changes.", "notcriticalErrorTitle": "Warning", + "titleDialogProtectedDocument": "The document is protected", + "textDialogProtectedOnlyView": "You may only view this document", + "textDialogProtectedFillForms": "You may only fill in forms in this document", + "textDialogProtectedChangesTracked": "You may edit this document, but all changes will be tracked", + "textDialogProtectedEditComments": "You may only insert comments to this document", "SDK": { " -Section ": " -Section ", "above": "above", @@ -661,6 +667,25 @@ "textRightToLeft": "Right To Left", "textSearch": "Search", "textSettings": "Settings", + "textProtection": "Protection", + "textProtectDocument": "Protect Document", + "titleDialogUnprotect": "Unprotect Document", + "textDialogUnprotect": "Enter a password to unprotect document", + "textUnprotect": "Unprotect", + "textProtectTurnOff": "Protect turn off", + "textSetPassword": "Set Password", + "textTypeEditing": "Type Of Editing", + "textNoChanges": "No changes (Read only)", + "textFillingForms": "Filling forms", + "textTrackedChanges": "Tracked changes", + "textPassword": "Password", + "textVerify": "Verify", + "textSave": "Save", + "textRequired": "Required", + "textPasswordNotMatched": "Passwords Don’t Match", + "textEncryptFile": "Encrypt File", + "textRequirePassword": "Require Password", + "textChangePassword": "Change Password", "textShowNotification": "Show Notification", "textSpaces": "Spaces", "textSpellcheck": "Spell Checking", diff --git a/apps/documenteditor/mobile/src/components/FieldPassword/FieldPassword.jsx b/apps/documenteditor/mobile/src/components/FieldPassword/FieldPassword.jsx new file mode 100644 index 0000000000..8f40790944 --- /dev/null +++ b/apps/documenteditor/mobile/src/components/FieldPassword/FieldPassword.jsx @@ -0,0 +1,28 @@ +import React, { useState } from 'react'; +import { ListInput, Icon } from "framework7-react"; +import { Device } from '../../../../../common/mobile/utils/device'; + +const FieldPassword = ({ label, value, onInput, placeholder }) => { + const isIos = Device.ios; + const [isShowPassword, setShowPassword] = useState(false); + + const toggleShowPassword = () => { + setShowPassword(!isShowPassword); + }; + + return ( + + {/* */} + + ); +} + +export default FieldPassword; \ No newline at end of file diff --git a/apps/documenteditor/mobile/src/components/Snackbar/Snackbar.jsx b/apps/documenteditor/mobile/src/components/Snackbar/Snackbar.jsx index ae94a2109a..28c65f6db6 100644 --- a/apps/documenteditor/mobile/src/components/Snackbar/Snackbar.jsx +++ b/apps/documenteditor/mobile/src/components/Snackbar/Snackbar.jsx @@ -1,13 +1,27 @@ import React from 'react'; +import { CSSTransition } from "react-transition-group"; -const Snackbar = props => { +const Snackbar = ({ isShowSnackbar, message, closeCallback }) => { return ( -
-
-

{props.text}

+ { + if(!isAppearing) { + closeCallback(); + } + }} + > +
+
+

{message}

+
-
- ) + + ); } export default Snackbar; \ No newline at end of file diff --git a/apps/documenteditor/mobile/src/controller/ContextMenu.jsx b/apps/documenteditor/mobile/src/controller/ContextMenu.jsx index c1a797643b..0b538dc0ca 100644 --- a/apps/documenteditor/mobile/src/controller/ContextMenu.jsx +++ b/apps/documenteditor/mobile/src/controller/ContextMenu.jsx @@ -20,12 +20,13 @@ import EditorUIController from '../lib/patch'; displayMode: stores.storeReview.displayMode, dataDoc: stores.storeDocumentInfo.dataDoc, objects: stores.storeFocusObjects.settings, - isViewer: stores.storeAppOptions.isViewer + isViewer: stores.storeAppOptions.isViewer, + isProtected: stores.storeAppOptions.isProtected, + typeProtection: stores.storeAppOptions.typeProtection })) class ContextMenu extends ContextMenuController { constructor(props) { super(props); - // console.log('context menu controller created'); this.onApiShowComment = this.onApiShowComment.bind(this); this.onApiHideComment = this.onApiHideComment.bind(this); @@ -282,13 +283,15 @@ class ContextMenu extends ContextMenuController { } else { const { t } = this.props; const _t = t("ContextMenu", {returnObjects: true}); - const { canViewComments, canCoAuthoring, canComments, dataDoc } = this.props; + const { canViewComments, canCoAuthoring, canComments, dataDoc, isProtected, typeProtection } = this.props; const api = Common.EditorApi.get(); const inToc = api.asc_GetTableOfContentsPr(true); const stack = api.getSelectedElements(); const canCopy = api.can_CopyCut(); const docExt = dataDoc ? dataDoc.fileType : ''; + const isAllowedEditing = !isProtected || typeProtection === Asc.c_oAscEDocProtect.TrackedChanges; + const isAllowedCommenting = typeProtection === Asc.c_oAscEDocProtect.Comments; let isText = false, isObject = false, @@ -324,14 +327,14 @@ class ContextMenu extends ContextMenuController { } if (!isDisconnected) { - if (canFillForms && canCopy && !locked && (!isViewer || docExt === 'oform')) { + if (canFillForms && canCopy && !locked && (!isViewer || docExt === 'oform') && isAllowedEditing) { itemsIcon.push({ event: 'cut', icon: 'icon-cut' }); } - if (canFillForms && canCopy && !locked && (!isViewer || docExt === 'oform')) { + if (canFillForms && canCopy && !locked && (!isViewer || docExt === 'oform') && isAllowedEditing) { itemsIcon.push({ event: 'paste', icon: 'icon-paste' @@ -345,7 +348,7 @@ class ContextMenu extends ContextMenuController { }); } - if (api.can_AddQuotedComment() !== false && canCoAuthoring && canComments && !locked && !(!isText && isObject) && !isViewer && canEditComments) { + if (api.can_AddQuotedComment() !== false && canCoAuthoring && canComments && !locked && !(!isText && isObject) && !isViewer && canEditComments && (isAllowedEditing || isAllowedCommenting)) { itemsText.push({ caption: _t.menuAddComment, event: 'addcomment' @@ -358,13 +361,16 @@ class ContextMenu extends ContextMenuController { caption: _t.menuOpenLink, event: 'openlink' }); - itemsText.push({ - caption: t('ContextMenu.menuEditLink'), - event: 'editlink' - }); + + if(isAllowedEditing) { + itemsText.push({ + caption: t('ContextMenu.menuEditLink'), + event: 'editlink' + }); + } } - if(inToc && isEdit && !isViewer) { + if(inToc && isEdit && !isViewer && isAllowedEditing) { itemsText.push({ caption: t('ContextMenu.textRefreshEntireTable'), event: 'refreshEntireTable' diff --git a/apps/documenteditor/mobile/src/controller/Error.jsx b/apps/documenteditor/mobile/src/controller/Error.jsx index 8fe766a05e..28ad038b47 100644 --- a/apps/documenteditor/mobile/src/controller/Error.jsx +++ b/apps/documenteditor/mobile/src/controller/Error.jsx @@ -107,6 +107,10 @@ const ErrorController = inject('storeAppOptions','storeDocumentInfo')(({storeApp config.msg = _t.errorFilePassProtect; break; + case Asc.c_oAscError.ID.PasswordIsNotCorrect: + config.msg = t('Error.errorPasswordIsNotCorrect'); + break; + case Asc.c_oAscError.ID.StockChartError: config.msg = _t.errorStockChart; break; diff --git a/apps/documenteditor/mobile/src/controller/Main.jsx b/apps/documenteditor/mobile/src/controller/Main.jsx index 562aa80d70..40e57e3e50 100644 --- a/apps/documenteditor/mobile/src/controller/Main.jsx +++ b/apps/documenteditor/mobile/src/controller/Main.jsx @@ -758,9 +758,12 @@ class MainController extends Component { // Downloaded Advanced Options this.api.asc_registerCallback('asc_onAdvancedOptions', (type, advOptions, mode, formatOptions) => { - const {t} = this.props; + const { t } = this.props; const _t = t("Settings", { returnObjects: true }); + const storeAppOptions = this.props.storeAppOptions; + if(type == Asc.c_oAscAdvancedOptionsID.DRM) { + storeAppOptions.setEncryptionFile(true); onAdvancedOptions(type, _t, this._isDocReady, this.props.storeAppOptions.canRequestClose, this.isDRM); this.isDRM = true; } @@ -796,6 +799,22 @@ class MainController extends Component { const storeAppOptions = this.props.storeAppOptions; const props = this.getDocProps(true); const isProtected = props && (props.isReadOnly || props.isCommentsOnly || props.isFormsOnly || props.isReviewOnly); + let textWarningDialog; + + switch(props.type) { + case Asc.c_oAscEDocProtect.ReadOnly: + textWarningDialog = t('Main.textDialogProtectedOnlyView'); + break; + case Asc.c_oAscEDocProtect.Comments: + textWarningDialog = t('Main.textDialogProtectedEditComments'); + break; + case Asc.c_oAscEDocProtect.TrackedChanges: + textWarningDialog = t('Main.textDialogProtectedChangesTracked') + break; + case Asc.c_oAscEDocProtect.Forms: + textWarningDialog = t('Main.textDialogProtectedFillForms'); + break; + } storeAppOptions.setProtection(isProtected); props && this.applyRestrictions(props.type); @@ -803,8 +822,8 @@ class MainController extends Component { if(isProtected) { f7.dialog.create({ - title: t('Main.notcriticalErrorTitle'), - text: t('Main.textDocumentProtected'), + title: t('Main.titleDialogProtectedDocument'), + text: textWarningDialog, buttons: [ { text: t('Main.textOk') @@ -838,7 +857,7 @@ class MainController extends Component { if (!storeAppOptions || !storeAppOptions.isEdit && !storeAppOptions.isRestrictedEdit) return; - if (isUpdate || !this.state.docProtection) { + if (isUpdate || !this._state.docProtection) { const props = this.api.asc_getDocumentProtection(); const type = props ? props.asc_getEditType() : Asc.c_oAscEDocProtect.None; diff --git a/apps/documenteditor/mobile/src/controller/settings/DocumentProtection.jsx b/apps/documenteditor/mobile/src/controller/settings/DocumentProtection.jsx new file mode 100644 index 0000000000..2fd08ed41d --- /dev/null +++ b/apps/documenteditor/mobile/src/controller/settings/DocumentProtection.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Device } from '../../../../../common/mobile/utils/device'; +import { observer, inject } from "mobx-react"; +import ProtectionDocumentView from '../../view/settings/DocumentProtection'; +import { f7 } from "framework7-react"; + +class ProtectionDocumentController extends React.Component { + constructor(props) { + super(props); + this.onProtectDocument = this.onProtectDocument.bind(this); + } + + closeModal() { + if (Device.phone) { + f7.sheet.close('.settings-popup', false); + } else { + f7.popover.close('#settings-popover', false); + } + }; + + onProtectDocument(typeProtection, password) { + const api = Common.EditorApi.get(); + const appOptions = this.props.storeAppOptions; + const isViewer = appOptions.isViewer; + const protection = api.asc_getDocumentProtection() || new AscCommonWord.CDocProtect(); + + appOptions.setProtection(true); + appOptions.setTypeProtection(typeProtection); + + if(typeProtection !== Asc.c_oAscEDocProtect.TrackedChanges && !isViewer) { + appOptions.changeViewerMode(); + } + + protection.asc_setEditType(typeProtection); + protection.asc_setPassword(password); + api.asc_setDocumentProtection(protection); + + this.closeModal(); + }; + + render() { + return + } +} + +export default inject('storeAppOptions')(observer(ProtectionDocumentController)); \ No newline at end of file diff --git a/apps/documenteditor/mobile/src/controller/settings/FileEncryption.jsx b/apps/documenteditor/mobile/src/controller/settings/FileEncryption.jsx new file mode 100644 index 0000000000..ae80c5b4bb --- /dev/null +++ b/apps/documenteditor/mobile/src/controller/settings/FileEncryption.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { observer, inject } from "mobx-react"; +import EncryptionView from '../../view/settings/FileEncryption'; +import { withTranslation } from 'react-i18next'; +import { f7 } from "framework7-react"; +import { Device } from '../../../../../common/mobile/utils/device'; + +class FileEncryptionController extends React.Component { + constructor(props) { + super(props); + + this.deletePassword = this.deletePassword.bind(this); + this.setPassword = this.setPassword.bind(this); + } + + closeModal() { + if (Device.phone) { + f7.sheet.close('.settings-popup', false); + } else { + f7.popover.close('#settings-popover', false); + } + } + + setPassword(passwordValue) { + const api = Common.EditorApi.get(); + + api.asc_setCurrentPassword(passwordValue); + this.closeModal(); + } + + deletePassword() { + const api = Common.EditorApi.get(); + const appOptions = this.props.storeAppOptions; + + appOptions.setEncryptionFile(false); + api.asc_resetPassword(); + this.closeModal(); + } + + render() { + return ( + + ) + } +} + +export default inject('storeAppOptions')(observer(withTranslation()(FileEncryptionController))); \ No newline at end of file diff --git a/apps/documenteditor/mobile/src/controller/settings/Protection.jsx b/apps/documenteditor/mobile/src/controller/settings/Protection.jsx new file mode 100644 index 0000000000..f6a5ad2cf2 --- /dev/null +++ b/apps/documenteditor/mobile/src/controller/settings/Protection.jsx @@ -0,0 +1,70 @@ +import React, { useEffect, useState } from 'react'; +import { observer, inject } from "mobx-react"; +import ProtectionView from '../../view/settings/Protection'; +import { useTranslation } from 'react-i18next'; +import { f7 } from "framework7-react"; +import { Device } from '../../../../../common/mobile/utils/device'; +import Snackbar from '../../components/Snackbar/Snackbar'; + +const ProtectionController = props => { + const { t } = useTranslation(); + const [isSnackbarVisible, setSnackbarVisible] = useState(false); + + const onProtectClick = () => { + const api = Common.EditorApi.get(); + const appOptions = props.storeAppOptions; + const isProtected = appOptions.isProtected; + let propsProtection = api.asc_getDocumentProtection(); + const isPassword = propsProtection?.asc_getIsPassword(); + + if(isProtected) { + if(propsProtection && isPassword) { + f7.dialog.create({ + title: t('Settings.titleDialogUnprotect'), + text: t('Settings.textDialogUnprotect'), + content: Device.ios ? + '
' : '
', + cssClass: 'dlg-adv-options', + buttons: [ + { + text: t('Settings.textCancel') + }, + { + text: t('Settings.textOk'), + onClick: () => { + const passwordValue = document.querySelector('#protection-password')?.value; + + if(passwordValue) { + propsProtection.asc_setEditType(Asc.c_oAscEDocProtect.None); + propsProtection.asc_setPassword(passwordValue); + api.asc_setDocumentProtection(propsProtection); + } + } + } + ] + }).open(); + } else { + if (!propsProtection) + propsProtection = new AscCommonWord.CDocProtect(); + + appOptions.setTypeProtection(null); + propsProtection.asc_setEditType(Asc.c_oAscEDocProtect.None); + api.asc_setDocumentProtection(propsProtection); + + setSnackbarVisible(true); + } + } else { + f7.views.current.router.navigate('/protect'); + } + } + + return ( + <> + + setSnackbarVisible(false)} /> + + ); + +} + +export default inject('storeAppOptions')(observer(ProtectionController)); \ No newline at end of file diff --git a/apps/documenteditor/mobile/src/controller/settings/Settings.jsx b/apps/documenteditor/mobile/src/controller/settings/Settings.jsx index 8a319d35b6..d2f03cd158 100644 --- a/apps/documenteditor/mobile/src/controller/settings/Settings.jsx +++ b/apps/documenteditor/mobile/src/controller/settings/Settings.jsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import {f7} from 'framework7-react'; import { observer, inject } from "mobx-react"; import {Device} from '../../../../../common/mobile/utils/device'; - import SettingsView from "../../view/settings/Settings"; import {LocalStorage} from "../../../../../common/mobile/utils/LocalStorage.mjs"; diff --git a/apps/documenteditor/mobile/src/less/app-ios.less b/apps/documenteditor/mobile/src/less/app-ios.less index a30272e5a8..be54f83121 100644 --- a/apps/documenteditor/mobile/src/less/app-ios.less +++ b/apps/documenteditor/mobile/src/less/app-ios.less @@ -60,7 +60,7 @@ // Snackbar .snackbar { - max-width: 195px; + width: 195px; position: absolute; bottom: 24px; left: calc(50% - 195px / 2); diff --git a/apps/documenteditor/mobile/src/less/app-material.less b/apps/documenteditor/mobile/src/less/app-material.less index da45b41bd9..8b89dc0edc 100644 --- a/apps/documenteditor/mobile/src/less/app-material.less +++ b/apps/documenteditor/mobile/src/less/app-material.less @@ -110,7 +110,6 @@ .snackbar { position: absolute; width: 344px; - max-height: 48px; left: calc(50% - 344px / 2); bottom: 16px; background: #333333; diff --git a/apps/documenteditor/mobile/src/less/icons-common.less b/apps/documenteditor/mobile/src/less/icons-common.less index 78003fc6a0..c1b4cbb7af 100644 --- a/apps/documenteditor/mobile/src/less/icons-common.less +++ b/apps/documenteditor/mobile/src/less/icons-common.less @@ -55,6 +55,24 @@ i.icon { .encoded-svg-background(''); } + &.icon-protect-document { + width: 22px; + height: 22px; + .encoded-svg-mask(''); + } + + &.icon-protection { + width: 22px; + height: 22px; + .encoded-svg-mask(''); + } + + &.icon-encrypt-file { + width: 22px; + height: 22px; + .encoded-svg-mask(''); + } + &.icon-format-fb2 { width: 22px; height: 22px; diff --git a/apps/documenteditor/mobile/src/less/icons-material.less b/apps/documenteditor/mobile/src/less/icons-material.less index 2110bff3cf..9d3adb4741 100644 --- a/apps/documenteditor/mobile/src/less/icons-material.less +++ b/apps/documenteditor/mobile/src/less/icons-material.less @@ -79,7 +79,7 @@ height: 24px; .encoded-svg-mask('', @toolbar-icons); } - &.icon-back-reader-mode { + &.icon-check { width: 24px; height: 24px; .encoded-svg-mask('', @toolbar-icons); @@ -92,6 +92,11 @@ height: 24px; .encoded-svg-mask('', @fill-white); } + &.icon-check { + width: 24px; + height: 24px; + .encoded-svg-mask(''); + } &.icon-expand-down { width: 17px; height: 17px; diff --git a/apps/documenteditor/mobile/src/page/main.jsx b/apps/documenteditor/mobile/src/page/main.jsx index 99781564ca..3fefddf8f4 100644 --- a/apps/documenteditor/mobile/src/page/main.jsx +++ b/apps/documenteditor/mobile/src/page/main.jsx @@ -15,7 +15,7 @@ import { Toolbar } from "../controller/Toolbar"; import NavigationController from '../controller/settings/Navigation'; import { AddLinkController } from '../controller/add/AddLink'; import EditHyperlink from '../controller/edit/EditHyperlink'; -import Snackbar from "../components/Snackbar/Snackbar"; +import Snackbar from '../components/Snackbar/Snackbar'; class MainPage extends Component { constructor(props) { @@ -135,7 +135,8 @@ class MainPage extends Component { const disabledControls = storeToolbarSettings.disabledControls; const disabledSettings = storeToolbarSettings.disabledSettings; const isProtected = appOptions.isProtected; - const isFabShow = isViewer && !disabledSettings && !disabledControls && !isDisconnected && isAvailableExt && isEdit && !isProtected; + const typeProtection = appOptions.typeProtection; + const isFabShow = isViewer && !disabledSettings && !disabledControls && !isDisconnected && isAvailableExt && isEdit && (!isProtected || typeProtection === Asc.c_oAscEDocProtect.TrackedChanges); const config = appOptions.config; const isShowPlaceholder = !appOptions.isDocReady && (!config.customization || !(config.customization.loaderName || config.customization.loaderLogo)); @@ -205,23 +206,12 @@ class MainPage extends Component { {/* { Device.phone ? null : } */} - { - if(!isAppearing) { - this.setState({ - snackbarVisible: false - }); - } - }} - > - - + + this.handleOptionsViewClosed('snackbar')} + message={isMobileView ? t("Toolbar.textSwitchedMobileView") : t("Toolbar.textSwitchedStandardView")} + /> { !this.state.editOptionsVisible ? null : diff --git a/apps/documenteditor/mobile/src/store/appOptions.js b/apps/documenteditor/mobile/src/store/appOptions.js index 537f7df89a..fee89a196e 100644 --- a/apps/documenteditor/mobile/src/store/appOptions.js +++ b/apps/documenteditor/mobile/src/store/appOptions.js @@ -32,17 +32,33 @@ export class storeAppOptions { changeMobileView: action, isProtected: observable, - setProtection: action + setProtection: action, + + typeProtection: observable, + setTypeProtection: action, + + isFileEncrypted: observable, + setEncryptionFile: action }); } isEdit = false; + isFileEncrypted = false; + setEncryptionFile(value) { + this.isFileEncrypted = value; + } + isProtected = false; setProtection(value) { this.isProtected = value; } + typeProtection = null; + setTypeProtection(type) { + this.typeProtection = type; + } + isMobileView = true; changeMobileView() { this.isMobileView = !this.isMobileView; diff --git a/apps/documenteditor/mobile/src/view/Toolbar.jsx b/apps/documenteditor/mobile/src/view/Toolbar.jsx index e5de507955..1b941f45fa 100644 --- a/apps/documenteditor/mobile/src/view/Toolbar.jsx +++ b/apps/documenteditor/mobile/src/view/Toolbar.jsx @@ -44,7 +44,7 @@ const ToolbarView = props => { return ( - {!isViewer && props.turnOnViewerMode()}>} + {!isViewer && props.turnOnViewerMode()}>} {(props.isShowBack && isViewer) && } {(Device.ios && props.isEdit && !isViewer) && EditorUIController.getUndoRedo && EditorUIController.getUndoRedo({ disabledUndo: !props.isCanUndo || isDisconnected, diff --git a/apps/documenteditor/mobile/src/view/settings/DocumentProtection.jsx b/apps/documenteditor/mobile/src/view/settings/DocumentProtection.jsx new file mode 100644 index 0000000000..5f5d67d43b --- /dev/null +++ b/apps/documenteditor/mobile/src/view/settings/DocumentProtection.jsx @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import { observer, inject } from "mobx-react"; +import { Device } from '../../../../../common/mobile/utils/device'; +import { Page, Navbar, List, ListItem, BlockTitle, Toggle, NavRight, f7, Link, ListInput, Icon, Block } from "framework7-react"; +import { useTranslation } from "react-i18next"; +import FieldPassword from '../../components/FieldPassword/FieldPassword'; + +const ProtectionDocumentView = inject("storeAppOptions")(observer(props => { + const { t } = useTranslation(); + const _t = t("Settings", { returnObjects: true }); + const isIos = Device.ios; + const appOptions = props.storeAppOptions; + const typeProtection = appOptions.typeProtection; + const [stateTypeProtection, setStateTypeProtection] = useState(typeProtection); + const [isRequirePassword, setRequirePassword] = useState(false); + const [password, changePassword] = useState(''); + const [passwordRepeat, changeRepeationPassword] = useState(''); + const isDisabledProtection = isRequirePassword ? ((!password.length || !passwordRepeat.length) || stateTypeProtection === null) : stateTypeProtection === null; + + const showErrorDialog = () => { + f7.dialog.create({ + title: t('Settings.textPasswordNotMatched'), + buttons: [ + { + text: t('Settings.textOk') + } + ] + }).open(); + }; + + const changeHanlder = () => { + if(isRequirePassword && password !== passwordRepeat) { + showErrorDialog(); + } else { + props.onProtectDocument(stateTypeProtection, password); + } + } + + return ( + + + + + {Device.android && } + + + + + + { + setRequirePassword(!isRequirePassword); + changePassword(''); + changeRepeationPassword(''); + }} /> + + + {isRequirePassword && + <> + + changePassword(e.target.value)} + className={isIos ? 'list-input-right' : ''} + /> + changeRepeationPassword(e.target.value)} + className={isIos ? 'list-input-right' : ''} + /> + + +

If the password is forgotten or lost, it cannot be recovered.

+
+ + } + {t('Settings.textTypeEditing')} + + { + setStateTypeProtection(Asc.c_oAscEDocProtect.ReadOnly); + }}> + { + setStateTypeProtection(Asc.c_oAscEDocProtect.Forms); + }}> + { + setStateTypeProtection(Asc.c_oAscEDocProtect.TrackedChanges); + }}> + { + setStateTypeProtection(Asc.c_oAscEDocProtect.Comments); + }}> + + +

Allow only this type of editing in the document.

+
+
+ ) +})); + +export default ProtectionDocumentView; \ No newline at end of file diff --git a/apps/documenteditor/mobile/src/view/settings/FileEncryption.jsx b/apps/documenteditor/mobile/src/view/settings/FileEncryption.jsx new file mode 100644 index 0000000000..afe88bafb0 --- /dev/null +++ b/apps/documenteditor/mobile/src/view/settings/FileEncryption.jsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { observer, inject } from "mobx-react"; +import { Device } from '../../../../../common/mobile/utils/device'; +import { Page, Navbar, List, ListItem, BlockTitle, Toggle, NavRight, f7, Link, ListInput, Icon, Block } from "framework7-react"; +import { useTranslation } from "react-i18next"; + +const EncryptionView = inject("storeAppOptions")(observer(props => { + const { t } = useTranslation(); + const _t = t("Settings", { returnObjects: true }); + const isIos = Device.ios; + const appOptions = props.storeAppOptions; + const isFileEncrypted = appOptions.isFileEncrypted; + const [isRequiredPassword, setRequirePassword] = useState(isFileEncrypted); + const [password, changePassword] = useState(''); + const [passwordRepeat, repeatPassword] = useState(''); + const isDisabledEncryption = isFileEncrypted && !isRequiredPassword ? false : !password.length || !passwordRepeat.length; + + const showErrorDialog = () => { + f7.dialog.create({ + title: t('Settings.textPasswordNotMatched'), + buttons: [ + { + text: t('Settings.textOk') + } + ] + }).open(); + }; + + const changeHanlder = () => { + if(isFileEncrypted && !isRequiredPassword) { + props.deletePassword(); + } else if(password !== passwordRepeat) { + showErrorDialog(); + } else { + props.setPassword(password); + } + } + + return ( + + + + + {Device.android && } + + + + {isFileEncrypted && + <> + + + { + setRequirePassword(!isRequiredPassword); + }} /> + + + + } + {isFileEncrypted && isRequiredPassword && + {t('Settings.textChangePassword')} + } + {(isFileEncrypted && isRequiredPassword || !isFileEncrypted) && + <> + + changePassword(e.target.value)} + className={isIos ? 'list-input-right' : ''} + /> + repeatPassword(e.target.value)} + className={isIos ? 'list-input-right' : ''} + /> + + +

If the password is forgotten or lost, it cannot be recovered.

+
+ + } +
+ ) +})); + +export default EncryptionView; \ No newline at end of file diff --git a/apps/documenteditor/mobile/src/view/settings/Protection.jsx b/apps/documenteditor/mobile/src/view/settings/Protection.jsx new file mode 100644 index 0000000000..2c08af3d52 --- /dev/null +++ b/apps/documenteditor/mobile/src/view/settings/Protection.jsx @@ -0,0 +1,27 @@ +import React, { useState } from 'react'; +import { observer, inject } from "mobx-react"; +import { Page, Navbar, List, ListItem, BlockTitle, Toggle, NavRight, f7, Link, ListInput, Icon, Block } from "framework7-react"; +import { useTranslation } from "react-i18next"; + +const ProtectionView = inject("storeAppOptions")(observer(props => { + const { t } = useTranslation(); + const _t = t("Settings", { returnObjects: true }); + const appOptions = props.storeAppOptions; + const isProtected = appOptions.isProtected; + + return ( + + + + props.onProtectClick()} link="#"> + + + + + + + + ) +})); + +export default ProtectionView; \ No newline at end of file diff --git a/apps/documenteditor/mobile/src/view/settings/Settings.jsx b/apps/documenteditor/mobile/src/view/settings/Settings.jsx index 9400bc7bfc..fc0778c045 100644 --- a/apps/documenteditor/mobile/src/view/settings/Settings.jsx +++ b/apps/documenteditor/mobile/src/view/settings/Settings.jsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'; import {f7} from 'framework7-react'; import { observer, inject } from "mobx-react"; import {Device} from '../../../../../common/mobile/utils/device'; - import DocumentSettingsController from "../../controller/settings/DocumentSettings"; import DocumentInfoController from "../../controller/settings/DocumentInfo"; import { DownloadController } from "../../controller/settings/Download"; @@ -14,6 +13,9 @@ import { MacrosSettings, Direction } from "./ApplicationSettings"; import About from '../../../../../common/mobile/lib/view/About'; import NavigationController from '../../controller/settings/Navigation'; import SharingSettings from "../../../../../common/mobile/lib/view/SharingSettings"; +import ProtectionDocumentController from '../../controller/settings/DocumentProtection'; +import ProtectionController from '../../controller/settings/Protection'; +import FileEncryptionController from '../../controller/settings/FileEncryption'; const routes = [ { @@ -65,17 +67,31 @@ const routes = [ }, // Direction - { path: '/direction/', component: Direction }, // Sharing Settings - { path: '/sharing-settings/', component: SharingSettings + }, + + // Protection + { + path: '/protection', + component: ProtectionController + }, + { + path: '/protect', + component: ProtectionDocumentController + }, + + // Encryption + { + path: '/encrypt', + component: FileEncryptionController } ]; @@ -155,6 +171,11 @@ const SettingsList = inject("storeAppOptions", "storeReview")(observer(props => } + {_isEdit && + + + + } { if(Device.phone) { onOpenNavigation(); @@ -245,13 +266,14 @@ class SettingsView extends Component { render() { const show_popover = this.props.usePopover; + return ( show_popover ? this.props.closeOptions('settings')}> - + : this.props.closeOptions('settings')}> - + ) }