diff --git a/src/json/constant.ts b/src/json/constant.ts index 940dd29cbc..c13544fc37 100644 --- a/src/json/constant.ts +++ b/src/json/constant.ts @@ -104,25 +104,26 @@ export default { }, subId: { - search: 'search', - profile: 'profile', - deleted: 'deleted', - type: 'type', - typeStore: 'typeStore', - relation: 'relation', - relationStore: 'relationStore', - option: 'option', - store: 'store', - archive: 'archive', - sidebar: 'sidebar', - space: 'space', - fileManager: 'fileManager', - participant: 'participant', - subSpace: 'subSpace', - allObject: 'allObject', - library: 'library', - chatSpace: 'lastMessage', - template: 'template', + search: 'search', + profile: 'profile', + deleted: 'deleted', + type: 'type', + typeStore: 'typeStore', + relation: 'relation', + relationStore: 'relationStore', + option: 'option', + store: 'store', + archive: 'archive', + sidebar: 'sidebar', + space: 'space', + participant: 'participant', + subSpace: 'subSpace', + allObject: 'allObject', + library: 'library', + chatSpace: 'lastMessage', + template: 'template', + fileManagerSynced: 'fileManagerSynced', + fileManagerNotSynced: 'fileManagerNotSynced' }, typeKey: { diff --git a/src/json/relation.ts b/src/json/relation.ts index 32efef084c..4fc0c6bad5 100644 --- a/src/json/relation.ts +++ b/src/json/relation.ts @@ -178,7 +178,8 @@ export default { syncStatus: [ 'syncStatus', 'syncDate', - 'syncError' + 'syncError', + 'fileSyncStatus' ], pageCover: 'pageCover', diff --git a/src/json/text.json b/src/json/text.json index 7d4cd4d2d8..f97c9e5704 100644 --- a/src/json/text.json +++ b/src/json/text.json @@ -219,7 +219,10 @@ "commonUnmute": "Unmute", "commonDropFiles": "Drag and drop your files here", "commonObjectEmpty": "It's empty here.", + "commonSynced": "Synced", + "commonNotSynced": "Not synced", "commonNewVersion": "Version %s is out now", + "commonUpgrade": "✦ Upgrade", "pluralDay": "day|days", "pluralObject": "Object|Objects", @@ -245,6 +248,7 @@ "pluralVideo": "Video|Videos", "pluralAudio": "Audio|Audios", "pluralFile": "File|Files", + "pluralLCFile": "file|files", "pluralPdf": "Pdf|Pdfs", "pluralBookmark": "Bookmark|Bookmarks", "pluralAttachment": "Attachment|Attachments", @@ -570,7 +574,8 @@ "pageSettingsSpaceGeneral": "General", "pageSettingsSpaceRemoteStorage": "Remote Storage", - "pageSettingsSpaceCleanupSpaceFiles": "Clean up space files", + "pageSettingsSpaceSyncedFiles": "Synced files", + "pageSettingsSpaceNotSyncedFiles": "Not synced files", "pageSettingsSpaceIntegrations": "Integrations", "pageSettingsSpaceManageContent": "Content Model", @@ -896,8 +901,8 @@ "popupSettingsSpaceIndexCollaborationTitle": "Collaboration", "popupSettingsSpaceIndexManageSpaceTitle": "Space Preferences", - "popupSettingsSpaceIndexRemoteStorageUpgrade": "✦ Upgrade", "popupSettingsSpaceIndexStorageText": "You can store up to %s of your files on our encrypted backup node for free. If you reach the limit, files will be stored only locally.", + "popupSettingsSpaceIndexStorageIsFullText": "Some files couldn't be synced because your storage is full. Upgrade your plan or remove files to free up space.", "popupSettingsSpaceIndexStorageManageFiles": "Manage files", "popupSettingsSpaceIndexHomepageDescription": "Select an Object to show when you login", "popupSettingsSpaceIndexImport": "Import to Space", @@ -1828,7 +1833,6 @@ "menuSyncStatusInfoNetworkMessageOffline": "No connection", "menuSyncStatusInfoSelfTitle": "Self Host", - "menuSyncStatusInfoSelfMessageSynced": "Synced", "menuSyncStatusInfoSelfMessageSyncing": "Syncing...", "menuSyncStatusInfoSelfMessageError": "Error", "menuSyncStatusInfoLocalOnlyTitle": "Local Only", @@ -1839,6 +1843,10 @@ "menuSyncStatusEmptyLocal": "Your Vault is currently in local-only mode, so your data isn't being synced to Anytype nodes.", "menuSyncStatusEmpty": "There are no objects to show", + "menuSyncStatusIncentiveBannerTitle": "Files not synced", + "menuSyncStatusIncentiveBannerLabel": "%s %s couldn’t be synced because you’re run out of space. Please upgrade or clean up your storage.", + "menuSyncStatusIncentiveBannerReviewFiles": "Review Files", + "menuPublishTitle": "Publish to web", "menuPublishInfoTooltip": "Published object will be uploaded to our publishing server and will be accessible via the URL as a static, unencrypted HTML page. Linked objects will not be published.
Currently, not all blocks are supported for publishing, such as Queries, Collections and Properties. Default limit is 10MB per object for free plans and 100MB for paid plans.", "menuPublishLabelJoinSpace": "Join Space Button", @@ -1849,7 +1857,6 @@ "menuPublishButtonCopy": "Copy Web Link", "menuPublishLabelOffline": "No internet connection", "menuPublishBecomeMemberText": "Become a member to get a domain and increase publishing limits", - "menuPublishUpgrade": "✦ Upgrade", "previewEdit": "Edit Link", diff --git a/src/scss/component/sidebar/common.scss b/src/scss/component/sidebar/common.scss index c8942818c5..d87ebe55e4 100644 --- a/src/scss/component/sidebar/common.scss +++ b/src/scss/component/sidebar/common.scss @@ -9,7 +9,7 @@ .sidebarAnimation { transition: width $transitionSidebarTime linear; } #sidebarToggle, -#sidebarSync { position: fixed; top: 12px; z-index: 22; -webkit-app-region: no-drag; transition: none; } +#sidebarSync { position: fixed; top: 12px; z-index: 25; -webkit-app-region: no-drag; transition: none; } #sidebarToggle { backdrop-filter: blur(20px); left: 84px; background-image: url('~img/icon/widget/toggle0.svg'); } #sidebarToggle.sidebarAnimation, #sidebarSync.sidebarAnimation { transition: left $transitionSidebarTime linear; } diff --git a/src/scss/component/sidebar/settings.scss b/src/scss/component/sidebar/settings.scss index 558a4501d9..949c6c2f6c 100644 --- a/src/scss/component/sidebar/settings.scss +++ b/src/scss/component/sidebar/settings.scss @@ -54,6 +54,7 @@ .caption { color: var(--color-text-secondary); text-align: right; @include text-overflow-nw; } .caption.join { font-weight: 500; padding: 2px 8px; border-radius: 4px; @include text-small; background: var(--color-control-accent); color: var(--color-bg-primary); } + .caption.alert { width: 20px; height: 20px; line-height: 20px; text-align: center; background-color: var(--color-red); color: var(--color-control-bg); border-radius: 50%; } } .item.isTypeOrRelation { padding-left: 26px; } diff --git a/src/scss/menu/syncStatus.scss b/src/scss/menu/syncStatus.scss index 1cd5075366..0394b230cf 100644 --- a/src/scss/menu/syncStatus.scss +++ b/src/scss/menu/syncStatus.scss @@ -46,6 +46,14 @@ } } + .incentiveBanner { margin: 10px 16px 0px 16px; padding: 12px 16px 16px 16px; border-radius: 8px; background: linear-gradient(180deg, #FEE7E0 30%, #FFF6F3 100%); } + .incentiveBanner { + .buttons { padding-top: 12px; display: flex; gap: 8px; } + .buttons { + .button { width: 50%; } + } + } + .items { height: 100%; } .items { .sectionName { padding: 4px 8px; } diff --git a/src/scss/page/main/settings.scss b/src/scss/page/main/settings.scss index 5f057cf279..e1cef400c6 100644 --- a/src/scss/page/main/settings.scss +++ b/src/scss/page/main/settings.scss @@ -457,7 +457,8 @@ } } - .fileManagerWrapper { flex-grow: 1; display: flex; flex-direction: column; } + .fileManagerWrapper { flex-grow: 1; display: flex; flex-direction: column; margin-bottom: 24px; } + .fileManagerWrapper:last-child { margin-bottom: 0px; } .fileManagerWrapper { .title { flex-shrink: 0; } diff --git a/src/ts/component/list/objectManager.tsx b/src/ts/component/list/objectManager.tsx index 14b21444db..8d044a6f09 100644 --- a/src/ts/component/list/objectManager.tsx +++ b/src/ts/component/list/objectManager.tsx @@ -14,6 +14,7 @@ interface Props { textEmpty?: string; filters?: I.Filter[]; sorts?: I.Sort[]; + keys?: string[]; rowHeight?: number; sources?: string[]; collectionId?: string; @@ -74,6 +75,7 @@ const ObjectManager = observer(forwardRef(({ textEmpty = '', filters = [], sorts = [], + keys = [], rowHeight = 0, sources = [], collectionId = '', @@ -203,6 +205,7 @@ const ObjectManager = observer(forwardRef(({ U.Subscription.subscribe({ subId, sorts, + keys, filters: fl, ignoreArchived, ignoreHidden, @@ -482,4 +485,4 @@ const ObjectManager = observer(forwardRef(({ ); })); -export default ObjectManager; \ No newline at end of file +export default ObjectManager; diff --git a/src/ts/component/menu/publish.tsx b/src/ts/component/menu/publish.tsx index 1e943aa920..890124b49d 100644 --- a/src/ts/component/menu/publish.tsx +++ b/src/ts/component/menu/publish.tsx @@ -150,14 +150,11 @@ const MenuPublish = observer(forwardRef((props, ref) => { }; const onUpgrade = () => { - U.Object.openRoute( - { id: 'membership', layout: I.ObjectLayout.Settings }, - { onRouteChange: () => { S.Popup.open('membership', { data: { tier: I.TierType.Builder }}) } }, - ); + Action.membershipUpgrade(); analytics.event('ClickUpgradePlanTooltip', { type: 'publish' }); }; - const setSlugHander = v => setSlug(U.Common.slug(v)); + const setSlugHandler = v => setSlug(U.Common.slug(v)); let buttons = []; @@ -173,7 +170,7 @@ const MenuPublish = observer(forwardRef((props, ref) => { }; useEffect(() => { - setSlugHander(object.name); + setSlugHandler(object.name); if (isOnline) { loadStatus(); @@ -202,7 +199,7 @@ const MenuPublish = observer(forwardRef((props, ref) => { ref={inputRef} value={slug} focusOnMount={true} - onChange={(e, v) => setSlugHander(v)} + onChange={(e, v) => setSlugHandler(v)} maxLength={300} />
@@ -233,7 +230,7 @@ const MenuPublish = observer(forwardRef((props, ref) => { {!tier?.namesCount ? (
) : ''} diff --git a/src/ts/component/menu/syncStatus.tsx b/src/ts/component/menu/syncStatus.tsx index 173c5a7258..8adb5a76b8 100644 --- a/src/ts/component/menu/syncStatus.tsx +++ b/src/ts/component/menu/syncStatus.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; import { AutoSizer, CellMeasurer, InfiniteLoader, List, CellMeasurerCache } from 'react-virtualized'; -import { Title, Icon, IconObject, ObjectName, EmptySearch } from 'Component'; +import { Title, Icon, IconObject, ObjectName, EmptySearch, Label, Button } from 'Component'; import { I, S, U, J, Action, translate, analytics, Onboarding } from 'Lib'; interface State { @@ -39,6 +39,9 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component ) : ''} + {notSyncedCounter && canWrite ? ( +
+ + <Label text={U.Common.sprintf(translate('menuSyncStatusIncentiveBannerLabel'), notSyncedCounter, U.Common.plural(notSyncedCounter, translate('pluralLCFile')))} /> + <div className="buttons"> + <Button onClick={() => this.onIncentiveButtonClick('storage')} className="c28" text={translate('menuSyncStatusIncentiveBannerReviewFiles')} color="blank" /> + {isOwner ? <Button onClick={() => this.onIncentiveButtonClick('upgrade')} className="c28" text={translate('commonUpgrade')} /> : ''} + </div> + </div> + ) : ''} + {this.cache && items.length ? ( <div className="items"> <InfiniteLoader @@ -275,6 +289,20 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M }; }; + onIncentiveButtonClick (id: string) { + switch (id) { + case 'storage': { + U.Object.openAuto({ id: 'spaceStorageManager', layout: I.ObjectLayout.Settings }); + break; + }; + + case 'upgrade': { + Action.membershipUpgrade(); + break; + }; + }; + }; + load () { if (U.Data.isLocalNetwork()) { return; @@ -284,6 +312,7 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M { relationKey: 'resolvedLayout', condition: I.FilterCondition.NotIn, value: U.Object.getSystemLayouts() }, ]; const sorts = [ + { relationKey: 'fileSyncStatus', type: I.SortType.Custom, customOrder: [ I.FileSyncStatus.NotSynced, I.FileSyncStatus.Synced ] }, { relationKey: 'syncStatus', type: I.SortType.Custom, customOrder: [ I.SyncStatusObject.Syncing, I.SyncStatusObject.Queued, I.SyncStatusObject.Synced ] }, { relationKey: 'syncDate', type: I.SortType.Desc, includeTime: true }, ]; @@ -435,7 +464,7 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M }; case I.SyncStatusSpace.Synced: { - message = translate('menuSyncStatusInfoSelfMessageSynced'); + message = translate('commonSynced'); break; }; @@ -512,4 +541,4 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M }); -export default MenuSyncStatus; \ No newline at end of file +export default MenuSyncStatus; diff --git a/src/ts/component/page/main/settings/space/storage.tsx b/src/ts/component/page/main/settings/space/storage.tsx index 2bf7c449c9..384ae1efcc 100644 --- a/src/ts/component/page/main/settings/space/storage.tsx +++ b/src/ts/component/page/main/settings/space/storage.tsx @@ -8,7 +8,10 @@ const STORAGE_FULL = 0.7; const PageMainSettingsStorageManager = observer(class PageMainSettingsStorageManager extends React.Component<I.PageSettingsComponent, {}> { node = null; - refManager = null; + refManagers = { + synced: null, + notSynced: null, + }; constructor (props: I.PageSettingsComponent) { super(props); @@ -20,12 +23,14 @@ const PageMainSettingsStorageManager = observer(class PageMainSettingsStorageMan render () { const { spaceStorage } = S.Common; const { localUsage, bytesLimit } = spaceStorage; + const { notSyncedCounter } = S.Auth.getSyncStatus(); const spaces = U.Space.getList(); const usageCn = [ 'item' ]; const canWrite = U.Space.canMyParticipantWrite(); let bytesUsed = 0; let buttonUpgrade = null; + let label = U.Common.sprintf(translate(`popupSettingsSpaceIndexStorageText`), U.File.size(bytesLimit)); const progressSegments = (spaces || []).map(space => { const object: any = S.Common.spaceStorage.spaces.find(it => it.spaceId == space.targetSpaceId) || {}; @@ -43,48 +48,71 @@ const PageMainSettingsStorageManager = observer(class PageMainSettingsStorageMan if (isRed) { usageCn.push('red'); - buttonUpgrade = <Button className="payment" text={translate('popupSettingsSpaceIndexRemoteStorageUpgrade')} onClick={this.onUpgrade} />; + buttonUpgrade = <Button className="payment" text={translate('commonUpgrade')} onClick={this.onUpgrade} />; + label = translate('popupSettingsSpaceIndexStorageIsFullText'); }; - const buttons: I.ButtonComponent[] = [ - { icon: 'remove', text: translate('commonDeleteImmediately'), onClick: this.onRemove } - ]; - const filters: I.Filter[] = [ - { relationKey: 'fileSyncStatus', condition: I.FilterCondition.Equal, value: I.FileSyncStatus.Synced }, - ]; - const sorts: I.Sort[] = [ - { type: I.SortType.Desc, relationKey: 'sizeInBytes' }, - ]; + const Manager = (item: any) => { + const { refId } = item; + const buttons: I.ButtonComponent[] = [ + { icon: 'remove', text: translate('commonDeleteImmediately'), onClick: () => this.onRemove(refId) } + ]; + const filters: I.Filter[] = [ + { relationKey: 'fileSyncStatus', condition: I.FilterCondition.In, value: item.filters }, + ]; + const sorts: I.Sort[] = [ + { type: I.SortType.Desc, relationKey: 'sizeInBytes' }, + ]; + + return ( + <div className="fileManagerWrapper"> + <Title className="sub" text={item.title} /> + + <ListObjectManager + ref={ref => this.refManagers[refId] = ref} + subId={item.subId} + rowLength={2} + buttons={buttons} + info={I.ObjectManagerItemInfo.FileSize} + iconSize={18} + sorts={sorts} + keys={U.Subscription.syncStatusRelationKeys()} + filters={filters} + ignoreHidden={false} + ignoreArchived={false} + textEmpty={translate('popupSettingsSpaceStorageManagerEmptyLabel')} + /> + </div> + ); + }; return ( <div ref={ref => this.node = ref} className="wrap"> {buttonUpgrade} <Title text={translate(`pageSettingsSpaceRemoteStorage`)} /> - <Label text={U.Common.sprintf(translate(`popupSettingsSpaceIndexStorageText`), U.File.size(bytesLimit))} /> + <Label text={label} /> <div className={usageCn.join(' ')}> <ProgressBar segments={progressSegments} current={U.File.size(bytesUsed)} max={U.File.size(bytesLimit)} /> </div> + {notSyncedCounter && canWrite ? ( + <Manager + refId={'notSynced'} + subId={J.Constant.subId.fileManagerNotSynced} + title={translate('pageSettingsSpaceNotSyncedFiles')} + filters={[ I.FileSyncStatus.NotSynced ]} + /> + ) : ''} + {canWrite ? ( - <div className="fileManagerWrapper"> - <Title className="sub" text={translate('pageSettingsSpaceCleanupSpaceFiles')} /> - - <ListObjectManager - ref={ref => this.refManager = ref} - subId={J.Constant.subId.fileManager} - rowLength={2} - buttons={buttons} - info={I.ObjectManagerItemInfo.FileSize} - iconSize={18} - sorts={sorts} - filters={filters} - ignoreHidden={false} - ignoreArchived={false} - textEmpty={translate('popupSettingsSpaceStorageManagerEmptyLabel')} - /> - </div> + <Manager + refId={'synced'} + subId={J.Constant.subId.fileManagerSynced} + title={translate('pageSettingsSpaceSyncedFiles')} + filters={[ I.FileSyncStatus.Synced ]} + /> ) : ''} </div> ); @@ -94,14 +122,20 @@ const PageMainSettingsStorageManager = observer(class PageMainSettingsStorageMan analytics.event('ScreenSettingsSpaceStorageManager'); }; + componentWillUnmount () { + U.Subscription.destroyList([ J.Constant.subId.fileManagerSynced, J.Constant.subId.fileManagerNotSynced ]); + }; + onUpgrade () { Action.membershipUpgrade(); analytics.event('ClickUpgradePlanTooltip', { type: 'storage', route: analytics.route.settingsSpaceIndex }); }; - onRemove () { - Action.delete(this.refManager.getSelected(), analytics.route.settings, () => this.refManager?.selectionClear()); + onRemove (refId: string) { + const ref = this.refManagers[refId]; + + Action.delete(ref.getSelected(), analytics.route.settings, () => ref?.selectionClear()); }; resize () { diff --git a/src/ts/component/sidebar/page/settings/index.tsx b/src/ts/component/sidebar/page/settings/index.tsx index 9b2bfd4622..d4653c3c74 100644 --- a/src/ts/component/sidebar/page/settings/index.tsx +++ b/src/ts/component/sidebar/page/settings/index.tsx @@ -99,6 +99,10 @@ const SidebarSettingsIndex = observer(class SidebarSettingsIndex extends React.C }; }; + if (item.alert) { + caption = <div className="caption alert">{item.alert}</div>; + }; + return ( <div id={`item-${item.id}`} @@ -198,6 +202,7 @@ const SidebarSettingsIndex = observer(class SidebarSettingsIndex extends React.C }; getSpaceSettings () { + const { error, notSyncedCounter } = S.Auth.getSyncStatus(); const space = U.Space.getSpaceview(); const isEntrySpace = space.spaceAccessType == I.SpaceType.Personal; const canWrite = U.Space.canMyParticipantWrite(); @@ -220,7 +225,7 @@ const SidebarSettingsIndex = observer(class SidebarSettingsIndex extends React.C children: [ { id: 'spaceIndex', icon: 'space', name: translate('pageSettingsSpaceGeneral') }, isEntrySpace ? null : { id: 'spaceShare', icon: 'members', name: members.length > 1 ? translate('commonMembers') : translate('pageSettingsSpaceIndexInviteMembers') }, - { id: 'spaceStorageManager', icon: 'storage', name: translate('pageSettingsSpaceRemoteStorage') }, + { id: 'spaceStorageManager', icon: 'storage', name: translate('pageSettingsSpaceRemoteStorage'), alert: notSyncedCounter }, { id: 'archive', icon: 'bin', name: translate('commonBin') }, ].filter(it => it), }, diff --git a/src/ts/component/util/sync.tsx b/src/ts/component/util/sync.tsx index c6500b8d99..ab12d89c63 100644 --- a/src/ts/component/util/sync.tsx +++ b/src/ts/component/util/sync.tsx @@ -52,4 +52,4 @@ const Sync = observer(forwardRef<HTMLDivElement, Props>(({ })); -export default Sync; \ No newline at end of file +export default Sync; diff --git a/src/ts/interface/syncStatus.ts b/src/ts/interface/syncStatus.ts index fe743cd995..049366122f 100644 --- a/src/ts/interface/syncStatus.ts +++ b/src/ts/interface/syncStatus.ts @@ -35,6 +35,7 @@ export interface SyncStatus { p2p: P2PStatus; syncingCounter: number; devicesCounter: number; + notSyncedCounter: number; }; export enum P2PStatus { diff --git a/src/ts/lib/api/mapper.ts b/src/ts/lib/api/mapper.ts index a4fd2895a1..2ac43d13d3 100644 --- a/src/ts/lib/api/mapper.ts +++ b/src/ts/lib/api/mapper.ts @@ -1686,7 +1686,8 @@ export const Mapper = { error: obj.getError(), network: obj.getNetwork(), status: obj.getStatus(), - syncingCounter: obj.getSyncingobjectscounter() + syncingCounter: obj.getSyncingobjectscounter(), + notSyncedCounter: obj.getNotsyncedfilescounter() }; }, diff --git a/src/ts/store/auth.ts b/src/ts/store/auth.ts index aaacc71e07..ea7359f85c 100644 --- a/src/ts/store/auth.ts +++ b/src/ts/store/auth.ts @@ -124,6 +124,7 @@ class AuthStore { p2p: observable, syncingCounter: observable, devicesCounter: observable, + notSyncedCounter: observable, }); intercept(obj as any, change => U.Common.intercept(obj, change)); @@ -220,6 +221,7 @@ class AuthStore { p2p: I.P2PStatus.NotConnected, syncingCounter: 0, devicesCounter: 0, + notSyncedCounter: 0, }; };