fix: Fixes issue where lock screen would not use previously active theme (#2372)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { WebApplicationInterface } from './../../WebApplication/WebApplicationInterface'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter'
|
||||
import data from './testData'
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('AegisConverter', () => {
|
||||
'[{"service":"TestMail","account":"test@test.com","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"test@test.com","secret":"SOMESERVICESOMESERVICESOMESERVIC","notes":"Some other service"}]',
|
||||
)
|
||||
expect(result.content.noteType).toBe(NoteType.Authentication)
|
||||
expect(result.content.editorIdentifier).toBe(FeatureIdentifier.TokenVaultEditor)
|
||||
expect(result.content.editorIdentifier).toBe(NativeFeatureIdentifier.TYPES.TokenVaultEditor)
|
||||
})
|
||||
|
||||
it('should create note from entries without editor info', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
@@ -69,7 +69,7 @@ export class AegisToAuthenticatorConverter {
|
||||
references: [],
|
||||
...(addEditorInfo && {
|
||||
noteType: NoteType.Authentication,
|
||||
editorIdentifier: FeatureIdentifier.TokenVaultEditor,
|
||||
editorIdentifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import { FeatureStatus } from '@standardnotes/services'
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter'
|
||||
import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter'
|
||||
import { GoogleKeepConverter } from './GoogleKeepConverter/GoogleKeepConverter'
|
||||
@@ -64,7 +64,9 @@ export class Importer {
|
||||
async getPayloadsFromFile(file: File, type: NoteImportType): Promise<DecryptedTransferPayload[]> {
|
||||
if (type === 'aegis') {
|
||||
const isEntitledToAuthenticator =
|
||||
this.application.features.getFeatureStatus(FeatureIdentifier.TokenVaultEditor) === FeatureStatus.Entitled
|
||||
this.application.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(),
|
||||
) === FeatureStatus.Entitled
|
||||
return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)]
|
||||
} else if (type === 'google-keep') {
|
||||
return [await this.googleKeepConverter.convertGoogleKeepBackupFileToNote(file, true)]
|
||||
|
||||
58
packages/ui-services/src/Theme/ActiveThemeList.spec.ts
Normal file
58
packages/ui-services/src/Theme/ActiveThemeList.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ActiveThemeList } from './ActiveThemeList'
|
||||
import { ItemManagerInterface } from '@standardnotes/services'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
describe('ActiveThemeList', () => {
|
||||
let itemManager: ItemManagerInterface
|
||||
let list: ActiveThemeList
|
||||
|
||||
beforeEach(() => {
|
||||
itemManager = {} as ItemManagerInterface
|
||||
itemManager.findItem = jest.fn()
|
||||
|
||||
list = new ActiveThemeList(itemManager)
|
||||
})
|
||||
|
||||
it('should initialize with an empty list', () => {
|
||||
expect(list.getList()).toEqual([])
|
||||
})
|
||||
|
||||
it('should be empty initially', () => {
|
||||
expect(list.isEmpty()).toBe(true)
|
||||
})
|
||||
|
||||
it('should not have items that have not been added', () => {
|
||||
const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
|
||||
expect(list.has(uuid)).toBe(false)
|
||||
})
|
||||
|
||||
it('should add an item to the list', () => {
|
||||
const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
|
||||
list.add(uuid)
|
||||
expect(list.getList()).toContain(uuid)
|
||||
expect(list.has(uuid)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not add a duplicate item to the list', () => {
|
||||
const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
|
||||
list.add(uuid)
|
||||
list.add(uuid)
|
||||
expect(list.getList()).toEqual([uuid])
|
||||
})
|
||||
|
||||
it('should remove an item from the list', () => {
|
||||
const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
|
||||
list.add(uuid)
|
||||
list.remove(uuid)
|
||||
expect(list.getList()).not.toContain(uuid)
|
||||
expect(list.has(uuid)).toBe(false)
|
||||
})
|
||||
|
||||
it('should clear the list', () => {
|
||||
const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
|
||||
list.add(uuid)
|
||||
list.clear()
|
||||
expect(list.getList()).toEqual([])
|
||||
expect(list.has(uuid)).toBe(false)
|
||||
})
|
||||
})
|
||||
70
packages/ui-services/src/Theme/ActiveThemeList.ts
Normal file
70
packages/ui-services/src/Theme/ActiveThemeList.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { UIFeature, ThemeInterface } from '@standardnotes/models'
|
||||
import { ItemManagerInterface } from '@standardnotes/services'
|
||||
import { NativeFeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export class ActiveThemeList {
|
||||
private list: (NativeFeatureIdentifier | Uuid)[] = []
|
||||
|
||||
constructor(private items: ItemManagerInterface, initialList?: (NativeFeatureIdentifier | Uuid)[]) {
|
||||
if (initialList) {
|
||||
this.list = initialList
|
||||
}
|
||||
}
|
||||
|
||||
public getList(): (NativeFeatureIdentifier | Uuid)[] {
|
||||
return this.list.slice()
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return this.list.length === 0
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.list = []
|
||||
}
|
||||
|
||||
public has(candidate: NativeFeatureIdentifier | Uuid): boolean {
|
||||
for (const entry of this.list) {
|
||||
if (entry.equals(candidate)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public add(candidate: NativeFeatureIdentifier | Uuid): void {
|
||||
if (!this.has(candidate)) {
|
||||
this.list.push(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
public remove(candidate: NativeFeatureIdentifier | Uuid): void {
|
||||
this.list = this.list.filter((entry) => {
|
||||
return !entry.equals(candidate)
|
||||
})
|
||||
}
|
||||
|
||||
public asThemes(): UIFeature<ThemeFeatureDescription>[] {
|
||||
const results: UIFeature<ThemeFeatureDescription>[] = []
|
||||
|
||||
for (const entry of this.list) {
|
||||
if (entry instanceof Uuid) {
|
||||
const theme = this.items.findItem<ThemeInterface>(entry.value)
|
||||
if (theme) {
|
||||
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
|
||||
results.push(uiFeature)
|
||||
}
|
||||
} else {
|
||||
const theme = FindNativeTheme(entry.value)
|
||||
if (theme) {
|
||||
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
|
||||
results.push(uiFeature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
PrefKey,
|
||||
ThemeInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import {
|
||||
InternalEventBusInterface,
|
||||
ApplicationEvent,
|
||||
@@ -16,17 +15,19 @@ import {
|
||||
PreferencesServiceEvent,
|
||||
ComponentManagerInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { FeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features'
|
||||
import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface'
|
||||
import { AbstractUIServicee } from '../Abstract/AbstractUIService'
|
||||
import { GetAllThemesUseCase } from './GetAllThemesUseCase'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { ActiveThemeList } from './ActiveThemeList'
|
||||
|
||||
const CachedThemesKey = 'cachedThemes'
|
||||
const TimeBeforeApplyingColorScheme = 5
|
||||
const DefaultThemeIdentifier = 'Default'
|
||||
|
||||
export class ThemeManager extends AbstractUIServicee {
|
||||
private themesActiveInTheUI: string[] = []
|
||||
private themesActiveInTheUI: ActiveThemeList
|
||||
private lastUseDeviceThemeSettings = false
|
||||
|
||||
constructor(
|
||||
@@ -37,6 +38,23 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
) {
|
||||
super(application, internalEventBus)
|
||||
this.colorSchemeEventHandler = this.colorSchemeEventHandler.bind(this)
|
||||
this.themesActiveInTheUI = new ActiveThemeList(application.items)
|
||||
}
|
||||
|
||||
override deinit() {
|
||||
this.themesActiveInTheUI.clear()
|
||||
;(this.themesActiveInTheUI as unknown) = undefined
|
||||
;(this.preferences as unknown) = undefined
|
||||
;(this.components as unknown) = undefined
|
||||
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
if (mq.removeEventListener != undefined) {
|
||||
mq.removeEventListener('change', this.colorSchemeEventHandler)
|
||||
} else {
|
||||
mq.removeListener(this.colorSchemeEventHandler)
|
||||
}
|
||||
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
override async onAppStart() {
|
||||
@@ -66,20 +84,32 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
|
||||
let hasChange = false
|
||||
|
||||
const activeThemes = this.components.getActiveThemesIdentifiers()
|
||||
for (const uiActiveTheme of this.themesActiveInTheUI) {
|
||||
if (!activeThemes.includes(uiActiveTheme)) {
|
||||
this.deactivateThemeInTheUI(uiActiveTheme)
|
||||
const { features, uuids } = this.components.getActiveThemesIdentifiers()
|
||||
|
||||
const featuresList = new ActiveThemeList(this.application.items, features)
|
||||
const uuidsList = new ActiveThemeList(this.application.items, uuids)
|
||||
|
||||
for (const active of this.themesActiveInTheUI.getList()) {
|
||||
if (!featuresList.has(active) && !uuidsList.has(active)) {
|
||||
this.deactivateThemeInTheUI(active)
|
||||
hasChange = true
|
||||
}
|
||||
}
|
||||
|
||||
for (const activeTheme of activeThemes) {
|
||||
if (!this.themesActiveInTheUI.includes(activeTheme)) {
|
||||
const theme =
|
||||
FindNativeTheme(activeTheme as FeatureIdentifier) ??
|
||||
this.application.items.findItem<ThemeInterface>(activeTheme)
|
||||
for (const feature of features) {
|
||||
if (!this.themesActiveInTheUI.has(feature)) {
|
||||
const theme = FindNativeTheme(feature.value)
|
||||
if (theme) {
|
||||
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
|
||||
this.activateTheme(uiFeature)
|
||||
hasChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const uuid of uuids) {
|
||||
if (!this.themesActiveInTheUI.has(uuid)) {
|
||||
const theme = this.application.items.findItem<ThemeInterface>(uuid.value)
|
||||
if (theme) {
|
||||
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
|
||||
this.activateTheme(uiFeature)
|
||||
@@ -99,7 +129,7 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
switch (event) {
|
||||
case ApplicationEvent.SignedOut: {
|
||||
this.deactivateAllThemes()
|
||||
this.themesActiveInTheUI = []
|
||||
this.themesActiveInTheUI.clear()
|
||||
this.application?.removeValue(CachedThemesKey, StorageValueModes.Nonwrapped).catch(console.error)
|
||||
break
|
||||
}
|
||||
@@ -158,35 +188,13 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
}
|
||||
}
|
||||
|
||||
override deinit() {
|
||||
this.themesActiveInTheUI = []
|
||||
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
if (mq.removeEventListener != undefined) {
|
||||
mq.removeEventListener('change', this.colorSchemeEventHandler)
|
||||
} else {
|
||||
mq.removeListener(this.colorSchemeEventHandler)
|
||||
}
|
||||
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
private handleFeaturesAvailabilityChanged(): void {
|
||||
let hasChange = false
|
||||
|
||||
for (const themeUuid of this.themesActiveInTheUI) {
|
||||
const theme = this.application.items.findItem<ThemeInterface>(themeUuid)
|
||||
|
||||
if (!theme) {
|
||||
this.deactivateThemeInTheUI(themeUuid)
|
||||
hasChange = true
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const status = this.application.features.getFeatureStatus(theme.identifier)
|
||||
for (const theme of this.themesActiveInTheUI.asThemes()) {
|
||||
const status = this.application.features.getFeatureStatus(theme.uniqueIdentifier)
|
||||
if (status !== FeatureStatus.Entitled) {
|
||||
this.deactivateThemeInTheUI(theme.uuid)
|
||||
this.deactivateThemeInTheUI(theme.uniqueIdentifier)
|
||||
hasChange = true
|
||||
}
|
||||
}
|
||||
@@ -194,7 +202,7 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
const activeThemes = this.components.getActiveThemes()
|
||||
|
||||
for (const theme of activeThemes) {
|
||||
if (!this.themesActiveInTheUI.includes(theme.uniqueIdentifier)) {
|
||||
if (!this.themesActiveInTheUI.has(theme.uniqueIdentifier)) {
|
||||
this.activateTheme(theme)
|
||||
hasChange = true
|
||||
}
|
||||
@@ -245,7 +253,7 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
const preference = prefersDarkColorScheme ? PrefKey.AutoDarkThemeIdentifier : PrefKey.AutoLightThemeIdentifier
|
||||
|
||||
const preferenceDefault =
|
||||
preference === PrefKey.AutoDarkThemeIdentifier ? FeatureIdentifier.DarkTheme : DefaultThemeIdentifier
|
||||
preference === PrefKey.AutoDarkThemeIdentifier ? NativeFeatureIdentifier.TYPES.DarkTheme : DefaultThemeIdentifier
|
||||
|
||||
const usecase = new GetAllThemesUseCase(this.application.items)
|
||||
const { thirdParty, native } = usecase.execute({ excludeLayerable: false })
|
||||
@@ -289,21 +297,20 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
}
|
||||
|
||||
private deactivateAllThemes() {
|
||||
const activeThemes = this.themesActiveInTheUI.slice()
|
||||
|
||||
const activeThemes = this.themesActiveInTheUI.getList()
|
||||
for (const uuid of activeThemes) {
|
||||
this.deactivateThemeInTheUI(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
private activateTheme(theme: UIFeature<ThemeFeatureDescription>, skipEntitlementCheck = false) {
|
||||
if (this.themesActiveInTheUI.find((uuid) => uuid === theme.uniqueIdentifier)) {
|
||||
if (this.themesActiveInTheUI.has(theme.uniqueIdentifier)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!skipEntitlementCheck &&
|
||||
this.application.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled
|
||||
this.application.features.getFeatureStatus(theme.uniqueIdentifier) !== FeatureStatus.Entitled
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -313,14 +320,14 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
return
|
||||
}
|
||||
|
||||
this.themesActiveInTheUI.push(theme.uniqueIdentifier)
|
||||
this.themesActiveInTheUI.add(theme.uniqueIdentifier)
|
||||
|
||||
const link = document.createElement('link')
|
||||
link.href = url
|
||||
link.type = 'text/css'
|
||||
link.rel = 'stylesheet'
|
||||
link.media = 'screen,print'
|
||||
link.id = theme.uniqueIdentifier
|
||||
link.id = theme.uniqueIdentifier.value
|
||||
link.onload = () => {
|
||||
this.syncThemeColorMetadata()
|
||||
|
||||
@@ -336,20 +343,20 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
document.getElementsByTagName('head')[0].appendChild(link)
|
||||
}
|
||||
|
||||
private deactivateThemeInTheUI(uuid: string) {
|
||||
if (!this.themesActiveInTheUI.includes(uuid)) {
|
||||
private deactivateThemeInTheUI(id: NativeFeatureIdentifier | Uuid) {
|
||||
if (!this.themesActiveInTheUI.has(id)) {
|
||||
return
|
||||
}
|
||||
|
||||
const element = document.getElementById(uuid) as HTMLLinkElement
|
||||
const element = document.getElementById(id.value) as HTMLLinkElement
|
||||
if (element) {
|
||||
element.disabled = true
|
||||
element.parentNode?.removeChild(element)
|
||||
}
|
||||
|
||||
removeFromArray(this.themesActiveInTheUI, uuid)
|
||||
this.themesActiveInTheUI.remove(id)
|
||||
|
||||
if (this.themesActiveInTheUI.length === 0 && this.application.isNativeMobileWeb()) {
|
||||
if (this.themesActiveInTheUI.isEmpty() && this.application.isNativeMobileWeb()) {
|
||||
this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff')
|
||||
}
|
||||
}
|
||||
@@ -373,11 +380,16 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
}
|
||||
|
||||
private async cacheThemeState() {
|
||||
const themes = this.application.items.findItems<ThemeInterface>(this.themesActiveInTheUI)
|
||||
const themes = this.themesActiveInTheUI.asThemes()
|
||||
|
||||
const mapped = themes.map((theme) => {
|
||||
const payload = theme.payloadRepresentation()
|
||||
return CreateDecryptedLocalStorageContextPayload(payload)
|
||||
if (theme.isComponent) {
|
||||
const payload = theme.asComponent.payloadRepresentation()
|
||||
return CreateDecryptedLocalStorageContextPayload(payload)
|
||||
} else {
|
||||
const payload = theme.asFeatureDescription
|
||||
return payload
|
||||
}
|
||||
})
|
||||
|
||||
return this.application.setValue(CachedThemesKey, mapped, StorageValueModes.Nonwrapped)
|
||||
@@ -389,16 +401,25 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
StorageValueModes.Nonwrapped,
|
||||
)
|
||||
|
||||
if (cachedThemes) {
|
||||
const themes: ThemeInterface[] = []
|
||||
for (const cachedTheme of cachedThemes) {
|
||||
const payload = this.application.items.createPayloadFromObject(cachedTheme)
|
||||
const theme = this.application.items.createItemFromPayload<ThemeInterface>(payload)
|
||||
themes.push(theme)
|
||||
}
|
||||
return themes.map((theme) => new UIFeature<ThemeFeatureDescription>(theme))
|
||||
} else {
|
||||
if (!cachedThemes) {
|
||||
return []
|
||||
}
|
||||
|
||||
const features: UIFeature<ThemeFeatureDescription>[] = []
|
||||
|
||||
for (const cachedTheme of cachedThemes) {
|
||||
if ('uuid' in cachedTheme) {
|
||||
const payload = this.application.items.createPayloadFromObject(cachedTheme)
|
||||
const theme = this.application.items.createItemFromPayload<ThemeInterface>(payload)
|
||||
features.push(new UIFeature<ThemeFeatureDescription>(theme))
|
||||
} else if ('identifier' in cachedTheme) {
|
||||
const feature = FindNativeTheme((cachedTheme as ThemeFeatureDescription).identifier)
|
||||
if (feature) {
|
||||
features.push(new UIFeature<ThemeFeatureDescription>(feature))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return features
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user