chore: feature status in context of item (#2359)

This commit is contained in:
Mo
2023-07-14 11:32:28 -05:00
committed by GitHub
parent 1c8d2f4fb9
commit 1c7a215519
7 changed files with 106 additions and 101 deletions

View File

@@ -1,11 +1,11 @@
import { FeatureIdentifier } from '@standardnotes/features' import { FeatureIdentifier } from '@standardnotes/features'
import { ComponentInterface } from '@standardnotes/models' import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models'
import { FeatureStatus } from './FeatureStatus' import { FeatureStatus } from './FeatureStatus'
import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse' import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse'
export interface FeaturesClientInterface { export interface FeaturesClientInterface {
getFeatureStatus(featureId: FeatureIdentifier): FeatureStatus getFeatureStatus(featureId: FeatureIdentifier, options?: { inContextOfItem?: DecryptedItemInterface }): FeatureStatus
hasMinimumRole(role: string): boolean hasMinimumRole(role: string): boolean
hasFirstPartyOfflineSubscription(): boolean hasFirstPartyOfflineSubscription(): boolean

View File

@@ -126,24 +126,6 @@ export class SNComponentManager
window.addEventListener('message', this.onWindowMessage, true) window.addEventListener('message', this.onWindowMessage, true)
} }
get isDesktop(): boolean {
return this.environment === Environment.Desktop
}
get isMobile(): boolean {
return this.environment === Environment.Mobile
}
get thirdPartyComponents(): ComponentInterface[] {
return this.items.getDisplayableComponents()
}
thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[] {
return this.thirdPartyComponents.filter((component) => {
return component.area === area
})
}
override deinit(): void { override deinit(): void {
super.deinit() super.deinit()
@@ -172,11 +154,17 @@ export class SNComponentManager
;(this.onWindowMessage as unknown) = undefined ;(this.onWindowMessage as unknown) = undefined
} }
setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void { public setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void {
this.permissionDialogUIHandler = handler this.permissionDialogUIHandler = handler
this.runWithPermissionsUseCase.setPermissionDialogUIHandler(handler) this.runWithPermissionsUseCase.setPermissionDialogUIHandler(handler)
} }
public thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[] {
return this.items.getDisplayableComponents().filter((component) => {
return component.area === area
})
}
public createComponentViewer( public createComponentViewer(
component: UIFeature<IframeComponentFeatureDescription>, component: UIFeature<IframeComponentFeatureDescription>,
item: ComponentViewerItem, item: ComponentViewerItem,
@@ -217,7 +205,7 @@ export class SNComponentManager
removeFromArray(this.viewers, viewer) removeFromArray(this.viewers, viewer)
} }
setDesktopManager(desktopManager: DesktopManagerInterface): void { public setDesktopManager(desktopManager: DesktopManagerInterface): void {
this.desktopManager = desktopManager this.desktopManager = desktopManager
this.configureForDesktop() this.configureForDesktop()
} }
@@ -234,13 +222,14 @@ export class SNComponentManager
return return
} }
if (this.isDesktop) { if (this.desktopManager) {
const thirdPartyComponents = components.filter((component) => { const thirdPartyComponents = components.filter((component) => {
const nativeFeature = FindNativeFeature(component.identifier) const nativeFeature = FindNativeFeature(component.identifier)
return nativeFeature ? false : true return nativeFeature ? false : true
}) })
if (thirdPartyComponents.length > 0) { if (thirdPartyComponents.length > 0) {
this.desktopManager?.syncComponentsInstallation(thirdPartyComponents) this.desktopManager.syncComponentsInstallation(thirdPartyComponents)
} }
} }
@@ -304,7 +293,8 @@ export class SNComponentManager
} }
detectFocusChange = (): void => { detectFocusChange = (): void => {
const activeIframes = this.allComponentIframes() const activeIframes = Array.from(document.getElementsByTagName('iframe'))
for (const iframe of activeIframes) { for (const iframe of activeIframes) {
if (document.activeElement === iframe) { if (document.activeElement === iframe) {
setTimeout(() => { setTimeout(() => {
@@ -322,18 +312,19 @@ export class SNComponentManager
} }
onWindowMessage = (event: MessageEvent): void => { onWindowMessage = (event: MessageEvent): void => {
/** Make sure this message is for us */
const data = event.data as ComponentMessage const data = event.data as ComponentMessage
if (data.sessionKey) { if (data.sessionKey) {
this.log('Component manager received message', data) this.log('Component manager received message', data)
this.componentViewerForSessionKey(data.sessionKey)?.handleMessage(data) this.componentViewerForSessionKey(data.sessionKey)?.handleMessage(data)
} }
} }
configureForDesktop(): void { private configureForDesktop(): void {
this.desktopManager?.registerUpdateObserver((component: ComponentInterface) => { if (!this.desktopManager) {
/* Reload theme if active */ throw new Error('Desktop manager is not defined')
}
this.desktopManager.registerUpdateObserver((component: ComponentInterface) => {
const activeComponents = this.getActiveComponents() const activeComponents = this.getActiveComponents()
const isComponentActive = activeComponents.find((candidate) => candidate.uuid === component.uuid) const isComponentActive = activeComponents.find((candidate) => candidate.uuid === component.uuid)
if (isComponentActive && component.isTheme()) { if (isComponentActive && component.isTheme()) {
@@ -342,18 +333,18 @@ export class SNComponentManager
}) })
} }
postActiveThemesToAllViewers(): void { private postActiveThemesToAllViewers(): void {
for (const viewer of this.viewers) { for (const viewer of this.viewers) {
viewer.postActiveThemes() viewer.postActiveThemes()
} }
} }
urlForFeature(uiFeature: UIFeature<ComponentFeatureDescription>): string | undefined { public urlForFeature(uiFeature: UIFeature<ComponentFeatureDescription>): string | undefined {
const usecase = new GetFeatureUrl(this.desktopManager, this.environment, this.platform) const usecase = new GetFeatureUrl(this.desktopManager, this.environment, this.platform)
return usecase.execute(uiFeature) return usecase.execute(uiFeature)
} }
urlsForActiveThemes(): string[] { public urlsForActiveThemes(): string[] {
const themes = this.getActiveThemes() const themes = this.getActiveThemes()
const urls = [] const urls = []
for (const theme of themes) { for (const theme of themes) {
@@ -373,7 +364,7 @@ export class SNComponentManager
return this.viewers.find((viewer) => viewer.sessionKey === key) return this.viewers.find((viewer) => viewer.sessionKey === key)
} }
async toggleTheme(uiFeature: UIFeature<ThemeFeatureDescription>): Promise<void> { public async toggleTheme(uiFeature: UIFeature<ThemeFeatureDescription>): Promise<void> {
this.log('Toggling theme', uiFeature.uniqueIdentifier) this.log('Toggling theme', uiFeature.uniqueIdentifier)
if (this.isThemeActive(uiFeature)) { if (this.isThemeActive(uiFeature)) {
@@ -406,7 +397,7 @@ export class SNComponentManager
} }
} }
getActiveThemes(): UIFeature<ThemeFeatureDescription>[] { public getActiveThemes(): UIFeature<ThemeFeatureDescription>[] {
const activeThemesIdentifiers = this.getActiveThemesIdentifiers() const activeThemesIdentifiers = this.getActiveThemesIdentifiers()
const thirdPartyThemes = this.items.findItems<ThemeInterface>(activeThemesIdentifiers).map((item) => { const thirdPartyThemes = this.items.findItems<ThemeInterface>(activeThemesIdentifiers).map((item) => {
@@ -427,11 +418,11 @@ export class SNComponentManager
return entitledThemes return entitledThemes
} }
getActiveThemesIdentifiers(): string[] { public getActiveThemesIdentifiers(): string[] {
return this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] return this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []
} }
async toggleComponent(component: ComponentInterface): Promise<void> { public async toggleComponent(component: ComponentInterface): Promise<void> {
this.log('Toggling component', component.uuid) this.log('Toggling component', component.uuid)
if (this.isComponentActive(component)) { if (this.isComponentActive(component)) {
@@ -441,14 +432,6 @@ export class SNComponentManager
} }
} }
allComponentIframes(): HTMLIFrameElement[] {
return Array.from(document.getElementsByTagName('iframe'))
}
iframeForComponentViewer(viewer: ComponentViewer): HTMLIFrameElement | undefined {
return viewer.getIframe()
}
editorForNote(note: SNNote): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription> { editorForNote(note: SNNote): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription> {
const usecase = new EditorForNoteUseCase(this.items) const usecase = new EditorForNoteUseCase(this.items)
return usecase.execute(note) return usecase.execute(note)

View File

@@ -82,13 +82,12 @@ export class ComponentViewer implements ComponentViewerInterface {
private loggingEnabled = false private loggingEnabled = false
public identifier = nonSecureRandomIdentifier() public identifier = nonSecureRandomIdentifier()
private actionObservers: ActionObserver[] = [] private actionObservers: ActionObserver[] = []
private featureStatus: FeatureStatus
private removeFeaturesObserver: () => void private removeFeaturesObserver: () => void
private eventObservers: ComponentEventObserver[] = [] private eventObservers: ComponentEventObserver[] = []
private dealloced = false private dealloced = false
private window?: Window private window?: Window
private hidden = false
private readonly = false private readonly = false
public lockReadonly = false public lockReadonly = false
public sessionKey?: string public sessionKey?: string
@@ -132,20 +131,14 @@ export class ComponentViewer implements ComponentViewerInterface {
this.actionObservers.push(options.actionObserver) this.actionObservers.push(options.actionObserver)
} }
this.featureStatus = services.features.getFeatureStatus(componentOrFeature.featureIdentifier)
this.removeFeaturesObserver = services.features.addEventObserver((event) => { this.removeFeaturesObserver = services.features.addEventObserver((event) => {
if (this.dealloced) { if (this.dealloced) {
return return
} }
if (event === FeaturesEvent.FeaturesAvailabilityChanged) { if (event === FeaturesEvent.FeaturesAvailabilityChanged) {
const featureStatus = services.features.getFeatureStatus(componentOrFeature.featureIdentifier) this.postActiveThemes()
if (featureStatus !== this.featureStatus) { this.notifyEventObservers(ComponentViewerEvent.FeatureStatusUpdated)
this.featureStatus = featureStatus
this.postActiveThemes()
this.notifyEventObservers(ComponentViewerEvent.FeatureStatusUpdated)
}
} }
}) })
@@ -226,7 +219,19 @@ export class ComponentViewer implements ComponentViewerInterface {
} }
public getFeatureStatus(): FeatureStatus { public getFeatureStatus(): FeatureStatus {
return this.featureStatus return this.services.features.getFeatureStatus(this.componentOrFeature.featureIdentifier, {
inContextOfItem: this.getContextItem(),
})
}
private getContextItem(): DecryptedItemInterface | undefined {
if (isComponentViewerItemReadonlyItem(this.options.item)) {
return this.options.item.readonlyItem
}
const matchingItem = this.services.items.findItem(this.options.item.uuid)
return matchingItem
} }
private isOfflineRestricted(): boolean { private isOfflineRestricted(): boolean {
@@ -274,7 +279,7 @@ export class ComponentViewer implements ComponentViewerInterface {
this.componentOrFeature = item this.componentOrFeature = item
} }
handleChangesInItems( private handleChangesInItems(
items: (DecryptedItemInterface | DeletedItemInterface | EncryptedItemInterface)[], items: (DecryptedItemInterface | DeletedItemInterface | EncryptedItemInterface)[],
source: PayloadEmitSource, source: PayloadEmitSource,
sourceKey?: string, sourceKey?: string,
@@ -312,7 +317,7 @@ export class ComponentViewer implements ComponentViewerInterface {
} }
} }
sendManyItemsThroughBridge(items: (DecryptedItemInterface | DeletedItemInterface)[]): void { private sendManyItemsThroughBridge(items: (DecryptedItemInterface | DeletedItemInterface)[]): void {
const requiredPermissions: ComponentPermission[] = [ const requiredPermissions: ComponentPermission[] = [
{ {
name: ComponentAction.StreamItems, name: ComponentAction.StreamItems,
@@ -329,7 +334,7 @@ export class ComponentViewer implements ComponentViewerInterface {
) )
} }
sendContextItemThroughBridge(item: DecryptedItemInterface, source?: PayloadEmitSource): void { private sendContextItemThroughBridge(item: DecryptedItemInterface, source?: PayloadEmitSource): void {
const requiredContextPermissions = [ const requiredContextPermissions = [
{ {
name: ComponentAction.StreamContextItem, name: ComponentAction.StreamContextItem,
@@ -443,14 +448,7 @@ export class ComponentViewer implements ComponentViewerInterface {
* @param essential If the message is non-essential, no alert will be shown * @param essential If the message is non-essential, no alert will be shown
* if we can no longer find the window. * if we can no longer find the window.
*/ */
sendMessage(message: ComponentMessage | MessageReply, essential = true): void { private sendMessage(message: ComponentMessage | MessageReply, essential = true): void {
const permissibleActionsWhileHidden = [ComponentAction.ComponentRegistered, ComponentAction.ActivateThemes]
if (this.hidden && !permissibleActionsWhileHidden.includes(message.action)) {
this.log('Component disabled for current item, ignoring messages.', this.componentOrFeature.displayName)
return
}
if (!this.window && message.action === ComponentAction.Reply) { if (!this.window && message.action === ComponentAction.Reply) {
this.log('Component has been deallocated in between message send and reply', this.componentOrFeature, message) this.log('Component has been deallocated in between message send and reply', this.componentOrFeature, message)
return return
@@ -514,10 +512,6 @@ export class ComponentViewer implements ComponentViewerInterface {
}) })
} }
public getWindow(): Window | undefined {
return this.window
}
/** Called by client when the iframe is ready */ /** Called by client when the iframe is ready */
public setWindow(window: Window): void { public setWindow(window: Window): void {
if (this.window) { if (this.window) {
@@ -548,7 +542,7 @@ export class ComponentViewer implements ComponentViewerInterface {
this.postActiveThemes() this.postActiveThemes()
} }
postActiveThemes(): void { public postActiveThemes(): void {
const urls = this.config.componentManagerFunctions.urlsForActiveThemes() const urls = this.config.componentManagerFunctions.urlsForActiveThemes()
const data: MessageData = { const data: MessageData = {
themes: urls, themes: urls,
@@ -562,24 +556,6 @@ export class ComponentViewer implements ComponentViewerInterface {
this.sendMessage(message, false) this.sendMessage(message, false)
} }
/* A hidden component will not receive messages. However, when a component is unhidden,
* we need to send it any items it may have registered streaming for. */
public setHidden(hidden: boolean): void {
if (hidden) {
this.hidden = true
} else if (this.hidden) {
this.hidden = false
if (this.streamContextItemOriginalMessage) {
this.handleStreamContextItemMessage(this.streamContextItemOriginalMessage)
}
if (this.streamItems) {
this.handleStreamItemsMessage(this.streamItemsOriginalMessage as ComponentMessage)
}
}
}
handleMessage(message: ComponentMessage): void { handleMessage(message: ComponentMessage): void {
this.log('Handle message', message, this) this.log('Handle message', message, this)
if (!this.componentOrFeature) { if (!this.componentOrFeature) {
@@ -616,7 +592,7 @@ export class ComponentViewer implements ComponentViewerInterface {
} }
} }
handleStreamItemsMessage(message: ComponentMessage): void { private handleStreamItemsMessage(message: ComponentMessage): void {
const data = message.data as StreamItemsMessageData const data = message.data as StreamItemsMessageData
const types = data.content_types.filter((type) => AllowedBatchContentTypes.includes(type)).sort() const types = data.content_types.filter((type) => AllowedBatchContentTypes.includes(type)).sort()
const requiredPermissions = [ const requiredPermissions = [
@@ -643,7 +619,7 @@ export class ComponentViewer implements ComponentViewerInterface {
) )
} }
handleStreamContextItemMessage(message: ComponentMessage): void { private handleStreamContextItemMessage(message: ComponentMessage): void {
const requiredPermissions: ComponentPermission[] = [ const requiredPermissions: ComponentPermission[] = [
{ {
name: ComponentAction.StreamContextItem, name: ComponentAction.StreamContextItem,
@@ -671,7 +647,7 @@ export class ComponentViewer implements ComponentViewerInterface {
* Save items is capable of saving existing items, and also creating new ones * Save items is capable of saving existing items, and also creating new ones
* if they don't exist. * if they don't exist.
*/ */
handleSaveItemsMessage(message: ComponentMessage): void { private handleSaveItemsMessage(message: ComponentMessage): void {
let responsePayloads = message.data.items as IncomingComponentItemPayload[] let responsePayloads = message.data.items as IncomingComponentItemPayload[]
const requiredPermissions = [] const requiredPermissions = []
@@ -814,7 +790,7 @@ export class ComponentViewer implements ComponentViewerInterface {
) )
} }
handleCreateItemsMessage(message: ComponentMessage): void { private handleCreateItemsMessage(message: ComponentMessage): void {
let responseItems = (message.data.item ? [message.data.item] : message.data.items) as IncomingComponentItemPayload[] let responseItems = (message.data.item ? [message.data.item] : message.data.items) as IncomingComponentItemPayload[]
const uniqueContentTypes = uniqueArray( const uniqueContentTypes = uniqueArray(
@@ -884,7 +860,7 @@ export class ComponentViewer implements ComponentViewerInterface {
) )
} }
handleDeleteItemsMessage(message: ComponentMessage): void { private handleDeleteItemsMessage(message: ComponentMessage): void {
const data = message.data as DeleteItemsMessageData const data = message.data as DeleteItemsMessageData
const items = data.items.filter((item) => AllowedBatchContentTypes.includes(item.content_type)) const items = data.items.filter((item) => AllowedBatchContentTypes.includes(item.content_type))
@@ -932,7 +908,7 @@ export class ComponentViewer implements ComponentViewerInterface {
) )
} }
handleSetComponentPreferencesMessage(message: ComponentMessage): void { private handleSetComponentPreferencesMessage(message: ComponentMessage): void {
const noPermissionsRequired: ComponentPermission[] = [] const noPermissionsRequired: ComponentPermission[] = []
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
this.componentUniqueIdentifier, this.componentUniqueIdentifier,
@@ -949,7 +925,7 @@ export class ComponentViewer implements ComponentViewerInterface {
) )
} }
handleSetSizeEvent(message: ComponentMessage): void { private handleSetSizeEvent(message: ComponentMessage): void {
if (this.componentOrFeature.area !== ComponentArea.EditorStack) { if (this.componentOrFeature.area !== ComponentArea.EditorStack) {
return return
} }
@@ -967,7 +943,7 @@ export class ComponentViewer implements ComponentViewerInterface {
} }
} }
getIframe(): HTMLIFrameElement | undefined { private getIframe(): HTMLIFrameElement | undefined {
return Array.from(document.getElementsByTagName('iframe')).find( return Array.from(document.getElementsByTagName('iframe')).find(
(iframe) => iframe.dataset.componentViewerId === this.identifier, (iframe) => iframe.dataset.componentViewerId === this.identifier,
) )

View File

@@ -16,6 +16,7 @@ import {
PayloadEmitSource, PayloadEmitSource,
ComponentInterface, ComponentInterface,
ThemeInterface, ThemeInterface,
DecryptedItemInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { import {
AbstractService, AbstractService,
@@ -389,7 +390,10 @@ export class SNFeaturesService
return indexOfRoleToCheck <= highestUserRoleIndex return indexOfRoleToCheck <= highestUserRoleIndex
} }
public getFeatureStatus(featureId: FeatureIdentifier): FeatureStatus { public getFeatureStatus(
featureId: FeatureIdentifier,
options: { inContextOfItem?: DecryptedItemInterface } = {},
): FeatureStatus {
return this.getFeatureStatusUseCase.execute({ return this.getFeatureStatusUseCase.execute({
featureId, featureId,
firstPartyRoles: this.hasFirstPartyOnlineSubscription() firstPartyRoles: this.hasFirstPartyOnlineSubscription()
@@ -401,6 +405,7 @@ export class SNFeaturesService
firstPartyOnlineSubscription: this.hasFirstPartyOnlineSubscription() firstPartyOnlineSubscription: this.hasFirstPartyOnlineSubscription()
? this.subscriptions.getOnlineSubscription() ? this.subscriptions.getOnlineSubscription()
: undefined, : undefined,
inContextOfItem: options.inContextOfItem,
}) })
} }

View File

@@ -1,7 +1,7 @@
import { FeatureIdentifier } from '@standardnotes/features' import { FeatureIdentifier } from '@standardnotes/features'
import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services' import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services'
import { GetFeatureStatusUseCase } from './GetFeatureStatus' import { GetFeatureStatusUseCase } from './GetFeatureStatus'
import { ComponentInterface } from '@standardnotes/models' import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models'
jest.mock('@standardnotes/features', () => ({ jest.mock('@standardnotes/features', () => ({
FeatureIdentifier: { FeatureIdentifier: {
@@ -71,6 +71,34 @@ describe('GetFeatureStatusUseCase', () => {
}) })
describe('native features', () => { describe('native features', () => {
it('should return Entitled if the context item belongs to a shared vault and user does not have subscription', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })
expect(
usecase.execute({
featureId: 'nativeFeature',
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
inContextOfItem: { shared_vault_uuid: 'sharedVaultUuid' } as jest.Mocked<DecryptedItemInterface>,
}),
).toEqual(FeatureStatus.Entitled)
})
it('should return NoUserSubscription if the context item does not belong to a shared vault and user does not have subscription', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })
expect(
usecase.execute({
featureId: 'nativeFeature',
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
inContextOfItem: { shared_vault_uuid: undefined } as jest.Mocked<DecryptedItemInterface>,
}),
).toEqual(FeatureStatus.NoUserSubscription)
})
it('should return NoUserSubscription for native features without subscription and roles', () => { it('should return NoUserSubscription for native features without subscription and roles', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false }) ;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })

View File

@@ -1,4 +1,5 @@
import { AnyFeatureDescription, FeatureIdentifier, FindNativeFeature } from '@standardnotes/features' import { AnyFeatureDescription, FeatureIdentifier, FindNativeFeature } from '@standardnotes/features'
import { DecryptedItemInterface } from '@standardnotes/models'
import { Subscription } from '@standardnotes/security' import { Subscription } from '@standardnotes/security'
import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services' import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services'
import { convertTimestampToMilliseconds } from '@standardnotes/utils' import { convertTimestampToMilliseconds } from '@standardnotes/utils'
@@ -11,6 +12,7 @@ export class GetFeatureStatusUseCase {
firstPartyOnlineSubscription: Subscription | undefined firstPartyOnlineSubscription: Subscription | undefined
firstPartyRoles: { online: string[] } | { offline: string[] } | undefined firstPartyRoles: { online: string[] } | { offline: string[] } | undefined
hasPaidAnyPartyOnlineOrOfflineSubscription: boolean hasPaidAnyPartyOnlineOrOfflineSubscription: boolean
inContextOfItem?: DecryptedItemInterface
}): FeatureStatus { }): FeatureStatus {
if (this.isFreeFeature(dto.featureId as FeatureIdentifier)) { if (this.isFreeFeature(dto.featureId as FeatureIdentifier)) {
return FeatureStatus.Entitled return FeatureStatus.Entitled
@@ -33,6 +35,7 @@ export class GetFeatureStatusUseCase {
nativeFeature, nativeFeature,
firstPartyOnlineSubscription: dto.firstPartyOnlineSubscription, firstPartyOnlineSubscription: dto.firstPartyOnlineSubscription,
firstPartyRoles: dto.firstPartyRoles, firstPartyRoles: dto.firstPartyRoles,
inContextOfItem: dto.inContextOfItem,
}) })
} }
@@ -51,7 +54,15 @@ export class GetFeatureStatusUseCase {
nativeFeature: AnyFeatureDescription nativeFeature: AnyFeatureDescription
firstPartyOnlineSubscription: Subscription | undefined firstPartyOnlineSubscription: Subscription | undefined
firstPartyRoles: { online: string[] } | { offline: string[] } | undefined firstPartyRoles: { online: string[] } | { offline: string[] } | undefined
inContextOfItem?: DecryptedItemInterface
}): FeatureStatus { }): FeatureStatus {
if (dto.inContextOfItem) {
const isSharedVaultItem = dto.inContextOfItem.shared_vault_uuid !== undefined
if (isSharedVaultItem) {
return FeatureStatus.Entitled
}
}
if (!dto.firstPartyOnlineSubscription && !dto.firstPartyRoles) { if (!dto.firstPartyOnlineSubscription && !dto.firstPartyRoles) {
return FeatureStatus.NoUserSubscription return FeatureStatus.NoUserSubscription
} }

View File

@@ -75,7 +75,9 @@ export const SuperEditor: FunctionComponent<Props> = ({
const [featureStatus, setFeatureStatus] = useState<FeatureStatus>(FeatureStatus.Entitled) const [featureStatus, setFeatureStatus] = useState<FeatureStatus>(FeatureStatus.Entitled)
useEffect(() => { useEffect(() => {
setFeatureStatus(application.features.getFeatureStatus(FeatureIdentifier.SuperEditor)) setFeatureStatus(
application.features.getFeatureStatus(FeatureIdentifier.SuperEditor, { inContextOfItem: note.current }),
)
}, [application.features]) }, [application.features])
const commandService = useCommandService() const commandService = useCommandService()