chore: feature status in context of item (#2359)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user