diff --git a/ChangeLog.md b/ChangeLog.md index 392719bd..fd9bb7a0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -2,6 +2,7 @@ * **19.04.21** - Fixed a bug that the client video box is shown as active even though the client does not stream any video - Fixed a bug that the video fullscreen windows pops open when a client leaves/joins the channel + - Removed extra new line after blockquote for the markdown renderer * **05.04.21** - Fixed the mute but for the webclient @@ -9,7 +10,7 @@ - Improved the recorder API * **29.03.21** - - Accquiering the default input recorder when opening the settings + - Acquiring the default input recorder when opening the settings - Adding new modal Input Processing Properties for the native client - Fixed that you can't finish off the name editing by pressing enter diff --git a/shared/js/Bookmarks.ts b/shared/js/Bookmarks.ts index 835b9cfd..55d6e99b 100644 --- a/shared/js/Bookmarks.ts +++ b/shared/js/Bookmarks.ts @@ -1,6 +1,6 @@ import * as loader from "tc-loader"; import {Stage} from "tc-loader"; -import {WritableKeys} from "tc-shared/proto"; +import {ignorePromise, WritableKeys} from "tc-shared/proto"; import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "tc-shared/log"; import {guid} from "tc-shared/crypto/uid"; import {Registry} from "tc-events"; @@ -8,6 +8,7 @@ import {server_connections} from "tc-shared/ConnectionManager"; import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile"; import {ConnectionState} from "tc-shared/ConnectionHandler"; import * as _ from "lodash"; +import {getStorageAdapter} from "tc-shared/StorageAdapter"; type BookmarkBase = { readonly uniqueId: string, @@ -49,7 +50,7 @@ export type OrderedBookmarkEntry = { childCount: number, }; -const kLocalStorageKey = "bookmarks_v2"; +const kStorageKey = "bookmarks_v2"; export class BookmarkManager { readonly events: Registry; private readonly registeredBookmarks: BookmarkEntry[]; @@ -59,25 +60,24 @@ export class BookmarkManager { this.events = new Registry(); this.registeredBookmarks = []; this.defaultBookmarkCreated = false; - this.loadBookmarks(); } - private loadBookmarks() { - const bookmarksJson = localStorage.getItem(kLocalStorageKey); + async loadBookmarks() { + const bookmarksJson = await getStorageAdapter().get(kStorageKey); if(typeof bookmarksJson !== "string") { - const oldBookmarksJson = localStorage.getItem("bookmarks"); + const oldBookmarksJson = await getStorageAdapter().get("bookmarks"); if(typeof oldBookmarksJson === "string") { logDebug(LogCategory.BOOKMARKS, tr("Found no new bookmarks but found old bookmarks. Trying to import.")); try { this.importOldBookmarks(oldBookmarksJson); logInfo(LogCategory.BOOKMARKS, tr("Successfully imported %d old bookmarks."), this.registeredBookmarks.length); - this.saveBookmarks(); + await this.saveBookmarks(); } catch (error) { const saveKey = "bookmarks_v1_save_" + Date.now(); logError(LogCategory.BOOKMARKS, tr("Failed to import old bookmark data. Saving it as %s"), saveKey); - localStorage.setItem(saveKey, oldBookmarksJson); + await getStorageAdapter().set(saveKey, oldBookmarksJson); } finally { - localStorage.removeItem("bookmarks"); + await getStorageAdapter().delete("bookmarks"); } } } else { @@ -93,9 +93,9 @@ export class BookmarkManager { logTrace(LogCategory.BOOKMARKS, tr("Loaded %d bookmarks."), this.registeredBookmarks.length); } catch (error) { const saveKey = "bookmarks_v2_save_" + Date.now(); - logError(LogCategory.BOOKMARKS, tr("Failed to parse bookmarks. Saving them at %s and using a clean setup."), saveKey) - localStorage.setItem(saveKey, bookmarksJson); - localStorage.removeItem("bookmarks_v2"); + logError(LogCategory.BOOKMARKS, tr("Failed to parse bookmarks. Saving them at %s and using a clean setup."), saveKey); + await getStorageAdapter().set(saveKey, bookmarksJson); + await getStorageAdapter().delete(kStorageKey); } } @@ -118,8 +118,6 @@ export class BookmarkManager { defaultChannel: undefined, defaultChannelPasswordHash: undefined, }); - - this.saveBookmarks(); } } @@ -203,12 +201,12 @@ export class BookmarkManager { this.defaultBookmarkCreated = true; } - private saveBookmarks() { - localStorage.setItem(kLocalStorageKey, JSON.stringify({ + async saveBookmarks() { + await getStorageAdapter().set(kStorageKey, JSON.stringify({ version: 2, bookmarks: this.registeredBookmarks, defaultBookmarkCreated: this.defaultBookmarkCreated, - })) + })); } getRegisteredBookmarks() : BookmarkEntry[] { @@ -278,7 +276,7 @@ export class BookmarkManager { } as BookmarkInfo); this.registeredBookmarks.push(bookmark); this.events.fire("notify_bookmark_created", { bookmark }); - this.saveBookmarks(); + ignorePromise(this.saveBookmarks()); return bookmark; } @@ -294,7 +292,7 @@ export class BookmarkManager { } as BookmarkDirectory); this.registeredBookmarks.push(bookmark); this.events.fire("notify_bookmark_created", { bookmark }); - this.saveBookmarks(); + ignorePromise(this.saveBookmarks()); return bookmark; } @@ -322,7 +320,7 @@ export class BookmarkManager { children.pop_front(); this.events.fire("notify_bookmark_deleted", { bookmark: entry, children }); - this.saveBookmarks(); + ignorePromise(this.saveBookmarks()); } executeConnect(uniqueId: string, newTab: boolean) { @@ -441,7 +439,7 @@ export class BookmarkManager { return; } - this.saveBookmarks(); + ignorePromise(this.saveBookmarks()); this.events.fire("notify_bookmark_edited", { bookmark: bookmarkInfo, keys: editedKeys }); } @@ -518,6 +516,7 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { name: "initialize bookmarks", function: async () => { bookmarks = new BookmarkManager(); + await bookmarks.loadBookmarks(); (window as any).bookmarks = bookmarks; }, priority: 20 diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index b83ff81f..d111a559 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -1,7 +1,7 @@ import {AbstractServerConnection} from "./connection/ConnectionBase"; import {PermissionManager} from "./permission/PermissionManager"; import {GroupManager} from "./permission/GroupManager"; -import {ServerSettings, Settings, settings, StaticSettings} from "./settings"; +import {Settings, settings} from "./settings"; import {Sound, SoundManager} from "./audio/Sounds"; import {ConnectionProfile} from "./profiles/ConnectionProfile"; import {LogCategory, logError, logInfo, logTrace, logWarn} from "./log"; @@ -38,6 +38,8 @@ import {getDNSProvider} from "tc-shared/dns"; import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin"; import ipRegex from "ip-regex"; import * as htmltags from "./ui/htmltags"; +import {ServerSettings} from "tc-shared/ServerSettings"; +import {ignorePromise} from "tc-shared/proto"; assertMainApplication(); @@ -165,7 +167,7 @@ export class ConnectionHandler { private localClient: LocalClientEntry; private autoReconnectTimer: number; - private autoReconnectAttempt: boolean = false; + private isReconnectAttempt: boolean; private connectAttemptId: number = 1; private echoTestRunning = false; @@ -275,9 +277,9 @@ export class ConnectionHandler { return this.events_; } - async startConnectionNew(parameters: ConnectParameters, autoReconnectAttempt: boolean) { + async startConnectionNew(parameters: ConnectParameters, isReconnectAttempt: boolean) { this.cancelAutoReconnect(true); - this.autoReconnectAttempt = autoReconnectAttempt; + this.isReconnectAttempt = isReconnectAttempt; this.handleDisconnect(DisconnectReason.REQUESTED); const localConnectionAttemptId = ++this.connectAttemptId; @@ -368,7 +370,7 @@ export class ConnectionHandler { } } - if(this.autoReconnectAttempt) { + if(this.isReconnectAttempt) { /* this.currentConnectId = 0; */ /* Reconnect attempts are connecting to the last server. No need to update the general attempt id */ } else { @@ -382,24 +384,6 @@ export class ConnectionHandler { await this.serverConnection.connect(resolvedAddress, new HandshakeHandler(parameters)); } - async startConnection(addr: string, profile: ConnectionProfile, user_action: boolean, parameters: ConnectParametersOld) { - await this.startConnectionNew({ - profile: profile, - targetAddress: addr, - - nickname: parameters.nickname, - nicknameSpecified: true, - - serverPassword: parameters.password?.password, - serverPasswordHashed: parameters.password?.hashed, - - defaultChannel: parameters?.channel?.target, - defaultChannelPassword: parameters?.channel?.password, - - token: parameters.token - }, !user_action); - } - async disconnectFromServer(reason?: string) { this.cancelAutoReconnect(true); if(!this.connected) { @@ -473,7 +457,7 @@ export class ConnectionHandler { } */ - this.settings.setServer(this.channelTree.server.properties.virtualserver_unique_identifier); + this.settings.setServerUniqueId(this.channelTree.server.properties.virtualserver_unique_identifier); /* apply the server settings */ if(this.handlerState.channel_subscribe_all) { @@ -570,7 +554,7 @@ export class ConnectionHandler { this.sound.play(Sound.CONNECTION_REFUSED); break; case DisconnectReason.CONNECT_FAILURE: - if(this.autoReconnectAttempt) { + if(this.isReconnectAttempt) { autoReconnect = true; break; } @@ -656,13 +640,13 @@ export class ConnectionHandler { break; case DisconnectReason.CONNECTION_CLOSED: logError(LogCategory.CLIENT, tr("Lost connection to remote server!")); - if(!this.autoReconnectAttempt) { + if(!this.isReconnectAttempt) { createErrorModal( tr("Connection closed"), tr("The connection was closed by remote host") ).open(); + this.sound.play(Sound.CONNECTION_DISCONNECTED); } - this.sound.play(Sound.CONNECTION_DISCONNECTED); autoReconnect = true; break; @@ -673,6 +657,7 @@ export class ConnectionHandler { tr("Connection lost"), tr("Lost connection to remote host (Ping timeout)
Even possible?") ).open(); + autoReconnect = true; break; case DisconnectReason.SERVER_CLOSED: @@ -690,25 +675,15 @@ export class ConnectionHandler { case DisconnectReason.SERVER_REQUIRES_PASSWORD: this.log.log("server.requires.password", {}); - createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, async password => { + const reconnectParameters = this.generateReconnectParameters(); + createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => { if(typeof password !== "string") { return; } - const profile = this.serverConnection.handshake_handler().parameters.profile; - const cprops = this.reconnect_properties(profile); - cprops.password = { - password: await hashPassword(password), - hashed: true - }; - - if(this.currentConnectId >= 0) { - connectionHistory.updateConnectionServerPassword(this.currentConnectId, cprops.password.password) - .catch(error => { - logWarn(LogCategory.GENERAL, tr("Failed to update the connection server password: %o"), error); - }); - } - this.startConnection(this.channelTree.server.remote_address.host + ":" + this.channelTree.server.remote_address.port, profile, false, cprops); + reconnectParameters.serverPassword = password; + reconnectParameters.serverPasswordHashed = false; + ignorePromise(this.startConnectionNew(reconnectParameters, false)); }).open(); break; case DisconnectReason.CLIENT_KICKED: @@ -754,9 +729,7 @@ export class ConnectionHandler { this.channelTree.unregisterClient(this.localClient); /* if we dont unregister our client here the client will be destroyed */ this.channelTree.reset(); - if(this.serverConnection) { - this.serverConnection.disconnect(); - } + ignorePromise(this.serverConnection?.disconnect()); this.handlerState.lastChannelCodecWarned = 0; @@ -768,15 +741,13 @@ export class ConnectionHandler { this.log.log("reconnect.scheduled", {timeout: 5000}); logInfo(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms")); - const server_address = this.serverConnection.remote_address(); - const profile = this.serverConnection.handshake_handler().parameters.profile; - + const reconnectParameters = this.generateReconnectParameters(); this.autoReconnectTimer = setTimeout(() => { this.autoReconnectTimer = undefined; this.log.log("reconnect.execute", {}); logInfo(LogCategory.NETWORKING, tr("Reconnecting...")); - this.startConnection(server_address.host + ":" + server_address.port, profile, false, Object.assign(this.reconnect_properties(profile), {auto_reconnect_attempt: true})); + ignorePromise(this.startConnectionNew(reconnectParameters, true)); }, 5000); } @@ -995,23 +966,24 @@ export class ConnectionHandler { getVoiceRecorder() : RecorderProfile | undefined { return this.serverConnection?.getVoiceConnection().voiceRecorder(); } - reconnect_properties(profile?: ConnectionProfile) : ConnectParametersOld { - const name = (this.getClient() ? this.getClient().clientNickName() : "") || - (this.serverConnection?.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") || - StaticSettings.instance.static(Settings.KEY_CONNECT_USERNAME, profile ? profile.defaultUsername : undefined) || - "Another TeaSpeak user"; - - const targetChannel = this.getClient().currentChannel(); - const connectParameters = this.serverConnection.handshake_handler().parameters; - - return { - channel: targetChannel ? {target: "/" + targetChannel.channelId, password: targetChannel.getCachedPasswordHash()} : undefined, - nickname: name, - password: connectParameters.serverPassword ? { - password: connectParameters.serverPassword, - hashed: connectParameters.serverPasswordHashed - } : undefined + generateReconnectParameters() : ConnectParameters | undefined { + const baseProfile = this.serverConnection.handshake_handler()?.parameters; + if(!baseProfile) { + /* We never tried to connect to anywhere */ + return undefined; } + + baseProfile.nickname = this.getClient()?.clientNickName() || baseProfile.nickname; + baseProfile.nicknameSpecified = false; + + const targetChannel = this.getClient()?.currentChannel(); + if(targetChannel) { + baseProfile.defaultChannel = targetChannel.channelId; + baseProfile.defaultChannelPassword = targetChannel.getCachedPasswordHash(); + baseProfile.defaultChannelPasswordHashed = true; + } + + return baseProfile; } private async initializeWhisperSession(session: WhisperSession) : Promise { diff --git a/shared/js/ServerSettings.ts b/shared/js/ServerSettings.ts new file mode 100644 index 00000000..7893c1d3 --- /dev/null +++ b/shared/js/ServerSettings.ts @@ -0,0 +1,138 @@ +import {tr} from "tc-shared/i18n/localize"; +import {LogCategory, logError} from "tc-shared/log"; +import { + encodeSettingValueToString, + RegistryKey, + RegistryValueType, + resolveSettingKey, + ValuedRegistryKey +} from "tc-shared/settings"; +import {assertMainApplication} from "tc-shared/ui/utils"; + +assertMainApplication(); + +export class ServerSettings { + private cacheServer; + private settingsDestroyed; + + private serverUniqueId: string; + private serverSaveWorker: number; + private serverSettingsUpdated: boolean; + + constructor() { + this.cacheServer = {}; + this.serverSettingsUpdated = false; + this.settingsDestroyed = false; + + this.serverSaveWorker = setInterval(() => { + if(this.serverSettingsUpdated) { + this.save(); + } + }, 5 * 1000); + } + + destroy() { + this.settingsDestroyed = true; + + this.serverUniqueId = undefined; + this.cacheServer = undefined; + + clearInterval(this.serverSaveWorker); + this.serverSaveWorker = undefined; + } + + getValue(key: RegistryKey, defaultValue: DV) : V | DV; + getValue(key: ValuedRegistryKey, defaultValue?: V) : V; + getValue(key, defaultValue) { + if(this.settingsDestroyed) { + throw "destroyed"; + } + + if(arguments.length > 1) { + return resolveSettingKey(key, key => this.cacheServer[key], defaultValue); + } else if("defaultValue" in key) { + return resolveSettingKey(key, key => this.cacheServer[key], key.defaultValue); + } else { + debugger; + throw tr("missing default value"); + } + } + + setValue(key: RegistryKey, value?: T) { + if(this.settingsDestroyed) { + throw "destroyed"; + } + + if(this.cacheServer[key.key] === value) { + return; + } + + this.serverSettingsUpdated = true; + if(value === undefined || value === null) { + delete this.cacheServer[key.key]; + } else { + this.cacheServer[key.key] = encodeSettingValueToString(value); + } + + this.save(); + } + + setServerUniqueId(serverUniqueId: string) { + if(this.settingsDestroyed) { + throw "destroyed"; + } + + if(this.serverUniqueId === serverUniqueId) { + return; + } + + if(this.serverUniqueId) { + this.save(); + this.cacheServer = {}; + this.serverUniqueId = undefined; + } + this.serverUniqueId = serverUniqueId; + + if(this.serverUniqueId) { + const json = settingsStorage.get(serverUniqueId); + try { + this.cacheServer = JSON.parse(json); + } catch(error) { + logError(LogCategory.GENERAL, tr("Failed to load server settings for server %s!\nJson: %s\nError: %o"), serverUniqueId, json, error); + } + if(!this.cacheServer) { + this.cacheServer = {}; + } + } + } + + save() { + if(this.settingsDestroyed) { + throw "destroyed"; + } + this.serverSettingsUpdated = false; + + if(this.serverUniqueId) { + settingsStorage.set(this.serverUniqueId, JSON.stringify(this.cacheServer)); + } + } +} + +let settingsStorage: ServerSettingsStorage = new class implements ServerSettingsStorage { + get(serverUniqueId: string): string { + return localStorage.getItem("settings.server_" + serverUniqueId); + } + + set(serverUniqueId: string, value: string) { + localStorage.setItem("settings.server_" + serverUniqueId, value); + } +}; + +export interface ServerSettingsStorage { + get(serverUniqueId: string) : string; + set(serverUniqueId: string, value: string); +} + +export function setServerSettingsStorage(storage: ServerSettingsStorage) { + settingsStorage = storage; +} \ No newline at end of file diff --git a/shared/js/StorageAdapter.ts b/shared/js/StorageAdapter.ts new file mode 100644 index 00000000..9e37cd04 --- /dev/null +++ b/shared/js/StorageAdapter.ts @@ -0,0 +1,42 @@ +/** + * Application storage meant for small and medium large internal app states. + * Possible data would be non user editable cached values like auth tokens. + * Note: + * 1. Please consider using a Settings key first before using the storage adapter! + * 2. The values may not be synced across multiple window instances. + * Don't use this for IPC. + */ +export interface StorageAdapter { + has(key: string) : Promise; + get(key: string) : Promise; + set(key: string, value: string) : Promise; + delete(key: string) : Promise; +} + +class LocalStorageAdapter implements StorageAdapter { + async delete(key: string): Promise { + localStorage.removeItem(key); + } + + async get(key: string): Promise { + return localStorage.getItem(key); + } + + async has(key: string): Promise { + return localStorage.getItem(key) !== null; + } + + async set(key: string, value: string): Promise { + localStorage.setItem(key, value); + } + +} + +let instance: StorageAdapter = new LocalStorageAdapter(); +export function getStorageAdapter() : StorageAdapter { + return instance; +} + +export function setStorageAdapter(adapter: StorageAdapter) { + instance = adapter; +} \ No newline at end of file diff --git a/shared/js/conversations/PrivateConversationHistory.ts b/shared/js/conversations/PrivateConversationHistory.ts index 4a06954e..ef871196 100644 --- a/shared/js/conversations/PrivateConversationHistory.ts +++ b/shared/js/conversations/PrivateConversationHistory.ts @@ -4,6 +4,12 @@ import {tr} from "tc-shared/i18n/localize"; import {LogCategory, logDebug, logError, logInfo, logWarn} from "tc-shared/log"; import {ChatEvent} from "../ui/frames/side/AbstractConversationDefinitions"; +/* + * Note: + * In this file we're explicitly using the local storage because the index-db database cache is windows bound + * like the local storage. We don't need to use the storage adapter here. + */ + const clientUniqueId2StoreName = uniqueId => "conversation-" + uniqueId; let currentDatabase: IDBDatabase; @@ -163,6 +169,7 @@ async function doOpenDatabase(forceUpgrade: boolean) { fireDatabaseStateChanged(); } + /* localStorage access note, see file start */ let localVersion = parseInt(localStorage.getItem("indexeddb-private-conversations-version") || "0"); let upgradePerformed = false; @@ -198,6 +205,7 @@ async function doOpenDatabase(forceUpgrade: boolean) { openRequest.onsuccess = () => resolve(openRequest.result); }); + /* localStorage access note, see file start */ localStorage.setItem("indexeddb-private-conversations-version", database.version.toString()); if(!upgradePerformed && forceUpgrade) { logWarn(LogCategory.CHAT, tr("Opened private conversations database, with an update, but update didn't happened. Trying again.")); diff --git a/shared/js/i18n/localize.ts b/shared/js/i18n/localize.ts index f8bf002f..114dc595 100644 --- a/shared/js/i18n/localize.ts +++ b/shared/js/i18n/localize.ts @@ -1,9 +1,10 @@ import {LogCategory, logDebug, logError, logInfo, logTrace, logWarn} from "../log"; import {guid} from "../crypto/uid"; -import {Settings, StaticSettings} from "../settings"; +import {settings, Settings} from "../settings"; import * as loader from "tc-loader"; import {formatMessage, formatMessageString} from "../ui/frames/chat"; +/* FIXME: Use the storage adapter and not the local storage else settings might get lost! */ export interface TranslationKey { message: string; line?: number; @@ -175,8 +176,9 @@ async function load_repository0(repo: TranslationRepository, reload: boolean) { Object.assign(repo, info_json); } - if(!repo.unique_id) + if(!repo.unique_id) { repo.unique_id = guid(); + } repo.translations = repo.translations || []; repo.load_timestamp = Date.now(); @@ -208,8 +210,9 @@ export namespace config { const repository_config_key = "i18n.repository"; let _cached_repository_config: RepositoryConfig; export function repository_config() { - if(_cached_repository_config) + if(_cached_repository_config) { return _cached_repository_config; + } const config_string = localStorage.getItem(repository_config_key); let config: RepositoryConfig; @@ -224,7 +227,7 @@ export namespace config { if(config.repositories.length == 0) { //Add the default TeaSpeak repository - load_repository(StaticSettings.instance.static(Settings.KEY_I18N_DEFAULT_REPOSITORY)).then(repo => { + load_repository(settings.getValue(Settings.KEY_I18N_DEFAULT_REPOSITORY)).then(repo => { logInfo(LogCategory.I18N, tr("Successfully added default repository from \"%s\"."), repo.url); register_repository(repo); }).catch(error => { diff --git a/shared/js/profiles/ConnectionProfile.ts b/shared/js/profiles/ConnectionProfile.ts index ba84e1e1..c8b7f14d 100644 --- a/shared/js/profiles/ConnectionProfile.ts +++ b/shared/js/profiles/ConnectionProfile.ts @@ -10,6 +10,16 @@ import * as loader from "tc-loader"; import {Stage} from "tc-loader"; import {LogCategory, logDebug, logError} from "tc-shared/log"; import {tr} from "tc-shared/i18n/localize"; +import {getStorageAdapter} from "tc-shared/StorageAdapter"; +import {ignorePromise} from "tc-shared/proto"; +import {assertMainApplication} from "tc-shared/ui/utils"; + +/* + * We're loading & saving profiles with the StorageAdapter. + * We should only access it once. As well why would a renderer want to have access to the + * connect profiles manager? + */ +assertMainApplication(); export class ConnectionProfile { id: string; @@ -130,7 +140,7 @@ let availableProfiles_: ConnectionProfile[] = []; async function loadConnectProfiles() { availableProfiles_ = []; - const profiles_json = localStorage.getItem("profiles"); + const profiles_json = await getStorageAdapter().get("profiles"); let profiles_data: ProfilesData = (() => { try { return profiles_json ? JSON.parse(profiles_json) : {version: 0} as any; @@ -216,7 +226,7 @@ export function save() { version: 1, profiles: profiles }); - localStorage.setItem("profiles", data); + ignorePromise(getStorageAdapter().set("profiles", data)); } export function mark_need_save() { diff --git a/shared/js/profiles/identities/teaspeak-forum.ts b/shared/js/profiles/identities/teaspeak-forum.ts index cec81283..899db2fb 100644 --- a/shared/js/profiles/identities/teaspeak-forum.ts +++ b/shared/js/profiles/identities/teaspeak-forum.ts @@ -3,6 +3,9 @@ import * as loader from "tc-loader"; import * as fidentity from "./TeaForumIdentity"; import {LogCategory, logDebug, logError, logInfo, logWarn} from "../../log"; import {tr} from "tc-shared/i18n/localize"; +import {getStorageAdapter} from "tc-shared/StorageAdapter"; + +/* TODO: Properly redesign this whole system! */ declare global { interface Window { @@ -57,8 +60,9 @@ export namespace gcaptcha { } } - if(typeof(window.grecaptcha) === "undefined") + if(typeof(window.grecaptcha) === "undefined") { throw tr("failed to load recaptcha"); + } } export async function spawn(container: JQuery, key: string, callback_data: (token: string) => any) { @@ -79,7 +83,7 @@ export namespace gcaptcha { } } -function api_url() { +function getForumApiURL() { return settings.getValue(Settings.KEY_TEAFORO_URL); } @@ -125,13 +129,13 @@ export class Data { is_expired() : boolean { return this.parsed.data_age + 48 * 60 * 60 * 1000 < Date.now(); } should_renew() : boolean { return this.parsed.data_age + 24 * 60 * 60 * 1000 < Date.now(); } /* renew data all 24hrs */ } -let _data: Data | undefined; +let forumData: Data | undefined; export function logged_in() : boolean { - return !!_data && !_data.is_expired(); + return !!forumData && !forumData.is_expired(); } -export function data() : Data { return _data; } +export function data() : Data { return forumData; } export interface LoginResult { status: "success" | "captcha" | "error"; @@ -148,7 +152,7 @@ export async function login(username: string, password: string, captcha?: any) : try { response = await new Promise((resolve, reject) => { $.ajax({ - url: api_url() + "?web-api/v1/login", + url: getForumApiURL() + "?web-api/v1/login", type: "POST", cache: false, data: { @@ -209,10 +213,11 @@ export async function login(username: string, password: string, captcha?: any) : //document.cookie = "user_sign=" + response["sign"] + ";path=/"; try { - _data = new Data(response["auth-key"], response["data"], response["sign"]); - localStorage.setItem("teaspeak-forum-data", response["data"]); - localStorage.setItem("teaspeak-forum-sign", response["sign"]); - localStorage.setItem("teaspeak-forum-auth", response["auth-key"]); + forumData = new Data(response["auth-key"], response["data"], response["sign"]); + const adapter = getStorageAdapter(); + await adapter.set("teaspeak-forum-data", response["data"]); + await adapter.set("teaspeak-forum-sign", response["sign"]); + await adapter.set("teaspeak-forum-auth", response["auth-key"]); fidentity.update_forum(); } catch(error) { logError(LogCategory.GENERAL, tr("Failed to parse forum given data: %o"), error); @@ -227,19 +232,26 @@ export async function login(username: string, password: string, captcha?: any) : }; } +async function resetForumLocalData() { + const adapter = getStorageAdapter(); + await adapter.delete("teaspeak-forum-data"); + await adapter.delete("teaspeak-forum-sign"); + await adapter.delete("teaspeak-forum-auth"); +} + export async function renew_data() : Promise<"success" | "login-required"> { let response; try { response = await new Promise((resolve, reject) => { $.ajax({ - url: api_url() + "?web-api/v1/renew-data", + url: getForumApiURL() + "?web-api/v1/renew-data", type: "GET", cache: false, crossDomain: true, data: { - "auth-key": _data.auth_key + "auth-key": forumData.auth_key }, success: resolve, @@ -270,9 +282,9 @@ export async function renew_data() : Promise<"success" | "login-required"> { logDebug(LogCategory.GENERAL, tr("Renew succeeded. Parsing data.")); try { - _data = new Data(_data.auth_key, response["data"], response["sign"]); - localStorage.setItem("teaspeak-forum-data", response["data"]); - localStorage.setItem("teaspeak-forum-sign", response["sign"]); + forumData = new Data(forumData.auth_key, response["data"], response["sign"]); + await getStorageAdapter().set("teaspeak-forum-data", response["data"]); + await getStorageAdapter().set("teaspeak-forum-sign", response["sign"]); fidentity.update_forum(); } catch(error) { logError(LogCategory.GENERAL, tr("Failed to parse forum given data: %o"), error); @@ -283,21 +295,22 @@ export async function renew_data() : Promise<"success" | "login-required"> { } export async function logout() : Promise { - if(!logged_in()) + if(!logged_in()) { return; + } let response; try { response = await new Promise((resolve, reject) => { $.ajax({ - url: api_url() + "?web-api/v1/logout", + url: getForumApiURL() + "?web-api/v1/logout", type: "GET", cache: false, crossDomain: true, data: { - "auth-key": _data.auth_key + "auth-key": forumData.auth_key }, success: resolve, @@ -312,7 +325,7 @@ export async function logout() : Promise { } if(response["status"] !== "ok") { - logError(LogCategory.GENERAL, tr("Response status not okey. Error happend: %o"), response); + logError(LogCategory.GENERAL, tr("Response status not ok. Error happened: %o"), response); throw (response["errors"] || [])[0] || tr("Unknown error"); } @@ -323,10 +336,8 @@ export async function logout() : Promise { } } - _data = undefined; - localStorage.removeItem("teaspeak-forum-data"); - localStorage.removeItem("teaspeak-forum-sign"); - localStorage.removeItem("teaspeak-forum-auth"); + forumData = undefined; + await resetForumLocalData(); fidentity.update_forum(); } @@ -334,30 +345,28 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { name: "TeaForo initialize", priority: 10, function: async () => { - const raw_data = localStorage.getItem("teaspeak-forum-data"); - const raw_sign = localStorage.getItem("teaspeak-forum-sign"); - const forum_auth = localStorage.getItem("teaspeak-forum-auth"); - if(!raw_data || !raw_sign || !forum_auth) { - logInfo(LogCategory.GENERAL, tr("No TeaForo authentification found. TeaForo connection status: unconnected")); + const rawData = await getStorageAdapter().get("teaspeak-forum-data"); + const rawSign = await getStorageAdapter().get("teaspeak-forum-sign"); + const rawForumAuth = await getStorageAdapter().get("teaspeak-forum-auth"); + if(!rawData || !rawSign || !rawForumAuth) { + logInfo(LogCategory.GENERAL, tr("No TeaForo authentication found. TeaForo connection status: unconnected")); return; } try { - _data = new Data(forum_auth, raw_data, raw_sign); + forumData = new Data(rawForumAuth, rawData, rawSign); } catch(error) { logError(LogCategory.GENERAL, tr("Failed to initialize TeaForo connection from local data. Error: %o"), error); return; } - if(_data.should_renew()) { + if(forumData.should_renew()) { logInfo(LogCategory.GENERAL, tr("TeaForo data should be renewed. Executing renew.")); - renew_data().then(status => { + renew_data().then(async status => { if(status === "success") { logInfo(LogCategory.GENERAL, tr("TeaForo data has been successfully renewed.")); } else { logWarn(LogCategory.GENERAL, tr("Failed to renew TeaForo data. New login required.")); - localStorage.removeItem("teaspeak-forum-data"); - localStorage.removeItem("teaspeak-forum-sign"); - localStorage.removeItem("teaspeak-forum-auth"); + await resetForumLocalData(); } }).catch(error => { logWarn(LogCategory.GENERAL, tr("Failed to renew TeaForo data. An error occurred: %o"), error); @@ -365,23 +374,21 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { return; } - if(_data && _data.is_expired()) { + if(forumData && forumData.is_expired()) { logError(LogCategory.GENERAL, tr("TeaForo data is expired. TeaForo connection isn't available!")); } setInterval(() => { /* if we don't have any _data object set we could not renew anything */ - if(_data) { + if(forumData) { logInfo(LogCategory.IDENTITIES, tr("Renewing TeaForo data.")); - renew_data().then(status => { + renew_data().then(async status => { if(status === "success") { logInfo(LogCategory.IDENTITIES,tr("TeaForo data has been successfully renewed.")); } else { logWarn(LogCategory.IDENTITIES,tr("Failed to renew TeaForo data. New login required.")); - localStorage.removeItem("teaspeak-forum-data"); - localStorage.removeItem("teaspeak-forum-sign"); - localStorage.removeItem("teaspeak-forum-auth"); + await resetForumLocalData(); } }).catch(error => { logWarn(LogCategory.GENERAL, tr("Failed to renew TeaForo data. An error occurred: %o"), error); diff --git a/shared/js/proto.ts b/shared/js/proto.ts index 2d7fcb9d..ad2ccbdc 100644 --- a/shared/js/proto.ts +++ b/shared/js/proto.ts @@ -3,12 +3,10 @@ import "jsrender"; import {tr} from "./i18n/localize"; import {LogCategory, logError, logTrace} from "tc-shared/log"; -if(__build.target === "web") { - (window as any).$ = require("jquery"); - (window as any).jQuery = $; +(window as any).$ = require("jquery"); +(window as any).jQuery = $; - require("jsrender")($); -} +require("jsrender")($); declare global { function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; diff --git a/shared/js/settings.ts b/shared/js/settings.ts index ae8fec39..bbff1620 100644 --- a/shared/js/settings.ts +++ b/shared/js/settings.ts @@ -1,8 +1,14 @@ import {LogCategory, logError, logTrace} from "./log"; -import * as loader from "tc-loader"; import {Stage} from "tc-loader"; import {Registry} from "./events"; -import { tr } from "./i18n/localize"; +import {tr} from "./i18n/localize"; +import {CallOnce, ignorePromise} from "tc-shared/proto"; +import {getStorageAdapter} from "tc-shared/StorageAdapter"; +import * as loader from "tc-loader"; + +/* + * TODO: Sync settings across renderer instances + */ export type RegistryValueType = boolean | number | string | object; export type RegistryValueTypeNames = "boolean" | "number" | "string" | "object"; @@ -54,7 +60,7 @@ function decodeValueFromString(input: string, type: } } -function encodeValueToString(input: T) : string { +export function encodeSettingValueToString(input: T) : string { switch (typeof input) { case "string": return input; @@ -73,7 +79,7 @@ function encodeValueToString(input: T) : string { } } -function resolveKey( +export function resolveSettingKey( key: RegistryKey, resolver: (key: string) => string | undefined | null, defaultValue: DefaultType @@ -136,9 +142,9 @@ export class UrlParameterParser { getValue(key: ValuedRegistryKey, defaultValue?: V) : V; getValue(key: RegistryKey | ValuedRegistryKey, defaultValue: DV) : V | DV { if(arguments.length > 1) { - return resolveKey(key, key => this.getParameter(key), defaultValue); + return resolveSettingKey(key, key => this.getParameter(key), defaultValue); } else if("defaultValue" in key) { - return resolveKey(key, key => this.getParameter(key), key.defaultValue); + return resolveSettingKey(key, key => this.getParameter(key), key.defaultValue); } else { throw tr("missing value"); } @@ -152,7 +158,7 @@ export class UrlParameterBuilder { if(value === undefined) { delete this.parameters[key.key]; } else { - this.parameters[key.key] = encodeURIComponent(encodeValueToString(value)); + this.parameters[key.key] = encodeURIComponent(encodeSettingValueToString(value)); } } @@ -289,32 +295,6 @@ export namespace AppParameters { }; } -export class StaticSettings { - private static _instance: StaticSettings; - static get instance() : StaticSettings { - if(!this._instance) { - this._instance = new StaticSettings(true); - } - - return this._instance; - } - - protected staticValues = {}; - - protected constructor(_reserved = undefined) { } - - static(key: RegistryKey, defaultValue: DV) : V | DV; - static(key: ValuedRegistryKey, defaultValue?: V) : V; - - static(key: RegistryKey | ValuedRegistryKey, defaultValue: DV) : V | DV { - if(arguments.length > 1) { - return AppParameters.getValue(key, defaultValue); - } else { - return AppParameters.getValue(key as ValuedRegistryKey); - } - } -} - export interface SettingsEvents { notify_setting_changed: { setting: string, @@ -816,6 +796,18 @@ export class Settings { description: "The target speaker device id", } + static readonly KEY_UPDATER_LAST_USED_UI: RegistryKey = { + key: "updater_last_used_ui", + valueType: "string", + description: "Last used TeaSpeak UI version", + } + + static readonly KEY_UPDATER_LAST_USED_CLIENT: RegistryKey = { + key: "updater_last_used_client", + valueType: "string", + description: "Last used TeaSpeak Client version (TeaClient only)", + } + static readonly FN_LOG_ENABLED: (category: string) => RegistryKey = category => { return { key: "log." + category.toLowerCase() + ".enabled", @@ -925,44 +917,49 @@ export class Settings { return result; })(); - static initialize() { - settings = new Settings(); - (window as any).settings = settings; - (window as any).Settings = Settings; - } - - readonly events: Registry; - private readonly cacheGlobal = {}; + private settingsCache: any; private saveWorker: number; - private updated: boolean = false; + private updated: boolean; + + private saveState: "none" | "saving" | "saving-changed"; constructor() { this.events = new Registry(); - const json = localStorage.getItem("settings.global"); + this.updated = false; + this.saveState = "none"; + } + + @CallOnce + async initialize() { + const json = await getStorageAdapter().get("settings.global"); + try { - this.cacheGlobal = JSON.parse(json); + this.settingsCache = JSON.parse(json); } catch(error) { + this.settingsCache = {}; logError(LogCategory.GENERAL, tr("Failed to load global settings!\nJson: %s\nError: %o"), json, error); const show_popup = () => { //FIXME: Readd this //createErrorModal(tr("Failed to load global settings"), tr("Failed to load global client settings!\nLookup console for more information.")).open(); }; - if(!loader.finished()) + if(!loader.finished()) { loader.register_task(loader.Stage.LOADED, { priority: 0, name: "Settings error", function: async () => show_popup() }); - else + } else { show_popup(); + } } - if(!this.cacheGlobal) this.cacheGlobal = {}; + this.saveWorker = setInterval(() => { - if(this.updated) + if(this.updated) { this.save(); + } }, 5 * 1000); } @@ -970,9 +967,9 @@ export class Settings { getValue(key: ValuedRegistryKey, defaultValue?: V) : V; getValue(key: RegistryKey | ValuedRegistryKey, defaultValue: DV) : V | DV { if(arguments.length > 1) { - return resolveKey(key, key => this.cacheGlobal[key], defaultValue); + return resolveSettingKey(key, key => this.settingsCache[key], defaultValue); } else if("defaultValue" in key) { - return resolveKey(key, key => this.cacheGlobal[key], key.defaultValue); + return resolveSettingKey(key, key => this.settingsCache[key], key.defaultValue); } else { debugger; throw tr("missing default value"); @@ -984,21 +981,21 @@ export class Settings { value = undefined; } - if(this.cacheGlobal[key.key] === value) { + if(this.settingsCache[key.key] === value) { return; } - const oldValue = this.cacheGlobal[key.key]; + const oldValue = this.settingsCache[key.key]; if(value === undefined) { - delete this.cacheGlobal[key.key]; + delete this.settingsCache[key.key]; } else { - this.cacheGlobal[key.key] = encodeValueToString(value); + this.settingsCache[key.key] = encodeSettingValueToString(value); } this.updated = true; this.events.fire("notify_setting_changed", { mode: "global", - newValue: this.cacheGlobal[key.key], + newValue: this.settingsCache[key.key], oldValue: oldValue, setting: key.key, newCastedValue: value @@ -1018,121 +1015,49 @@ export class Settings { }) } - save() { - this.updated = false; - let global = JSON.stringify(this.cacheGlobal); - localStorage.setItem("settings.global", global); - if(localStorage.save) - localStorage.save(); - } -} - -export class ServerSettings { - private cacheServer = {}; - private serverUniqueId: string; - private serverSaveWorker: number; - private serverSettingsUpdated: boolean = false; - private _destroyed = false; - - constructor() { - this.serverSaveWorker = setInterval(() => { - if(this.serverSettingsUpdated) { - this.save(); - } - }, 5 * 1000); - } - - destroy() { - this._destroyed = true; - - this.serverUniqueId = undefined; - this.cacheServer = undefined; - - clearInterval(this.serverSaveWorker); - this.serverSaveWorker = undefined; - } - - getValue(key: RegistryKey, defaultValue: DV) : V | DV; - getValue(key: ValuedRegistryKey, defaultValue?: V) : V; - getValue(key, defaultValue) { - if(this._destroyed) { - throw "destroyed"; - } - - if(arguments.length > 1) { - return resolveKey(key, key => this.cacheServer[key], defaultValue); - } else if("defaultValue" in key) { - return resolveKey(key, key => this.cacheServer[key], key.defaultValue); - } else { - debugger; - throw tr("missing default value"); - } - } - - setValue(key: RegistryKey, value?: T) { - if(this._destroyed) { - throw "destroyed"; - } - - if(this.cacheServer[key.key] === value) { + private async doSave() { + if(this.saveState === "none") { return; } - this.serverSettingsUpdated = true; - if(value === undefined || value === null) { - delete this.cacheServer[key.key]; - } else { - this.cacheServer[key.key] = encodeValueToString(value); - } + do { + this.saveState = "saving"; - if(UPDATE_DIRECT) { - this.save(); - } - } - - setServer(server_unique_id: string) { - if(this._destroyed) throw "destroyed"; - if(this.serverUniqueId) { - this.save(); - this.cacheServer = {}; - this.serverUniqueId = undefined; - } - this.serverUniqueId = server_unique_id; - - if(this.serverUniqueId) { - - const json = localStorage.getItem("settings.server_" + server_unique_id); try { - this.cacheServer = JSON.parse(json); - } catch(error) { - logError(LogCategory.GENERAL, tr("Failed to load server settings for server %s!\nJson: %s\nError: %o"), server_unique_id, json, error); + await getStorageAdapter().set("settings.global", JSON.stringify(this.settingsCache)); + } catch (error) { + logError(LogCategory.GENERAL, tr("Failed to save global settings: %o"), error); } - if(!this.cacheServer) - this.cacheServer = {}; - } + } while(this.saveState !== "saving"); + + this.saveState = "none"; } save() { - if(this._destroyed) { - throw "destroyed"; - } - this.serverSettingsUpdated = false; + switch (this.saveState) { + case "saving": + case "saving-changed": + this.saveState = "saving-changed"; + return; - if(this.serverUniqueId) { - let server = JSON.stringify(this.cacheServer); - localStorage.setItem("settings.server_" + this.serverUniqueId, server); - - if(localStorage.save) { - localStorage.save(); - } + default: + case "none": + this.saveState = "saving"; + break; } + + ignorePromise(this.doSave()); } } -export let settings: Settings = null; - +export let settings: Settings; loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { priority: 1000, name: "Settings initialize", - function: async () => Settings.initialize() + function: async () => { + settings = new Settings(); + await settings.initialize(); + (window as any).settings = settings; + (window as any).Settings = Settings; + } }) \ No newline at end of file diff --git a/shared/js/text/markdown.ts b/shared/js/text/markdown.ts index 1a18d50d..225c2a4b 100644 --- a/shared/js/text/markdown.ts +++ b/shared/js/text/markdown.ts @@ -95,7 +95,6 @@ export class MD2BBCodeRenderer { "hr": () => "[hr]", - //> Experience real-time editing with Remarkable! "blockquote_open": () => "[quote]", "blockquote_close": () => "[/quote]" }; @@ -162,6 +161,8 @@ export function renderMarkdownAsBBCode(message: string, textProcessor: (text: st md2bbCodeRenderer.reset(); let result = remarkableRenderer.render(message); + /* Replace the extra \n after a quote since quotes itself are blocks and not inline blocks */ + result = result.replace(/\[\/quote]\n/g, "[/quote]"); logTrace(LogCategory.CHAT, tr("Markdown render result:\n%s"), result); return result; } \ No newline at end of file diff --git a/shared/js/ui/modal/about/Controller.ts b/shared/js/ui/modal/about/Controller.ts index 6b666176..3123d7d4 100644 --- a/shared/js/ui/modal/about/Controller.ts +++ b/shared/js/ui/modal/about/Controller.ts @@ -4,7 +4,11 @@ import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable"; import {spawnModal} from "tc-shared/ui/react-elements/modal"; import {CallOnce, ignorePromise} from "tc-shared/proto"; import {getBackend} from "tc-shared/backend"; +import {getStorageAdapter} from "tc-shared/StorageAdapter"; +/** + * We're using the storage adapter since we don't + */ class Controller { readonly events: Registry; readonly variables: IpcUiVariableProvider; @@ -32,18 +36,18 @@ class Controller { this.variables.setVariableProvider("eggShown", () => this.eggShown); this.variables.setVariableEditor("eggShown", newValue => { this.eggShown = newValue; }); - this.events.on("action_update_high_score", event => { - let highScore = parseInt(localStorage.getItem("ee-snake-high-score")); + this.events.on("action_update_high_score", async event => { + let highScore = parseInt(await getStorageAdapter().get("ee-snake-high-score")); if(!isNaN(highScore) && highScore >= event.score) { /* No change */ return; } - localStorage.setItem("ee-snake-high-score", event.score.toString()); + await getStorageAdapter().set("ee-snake-high-score", event.score.toString()); }); - this.events.on("query_high_score", () => { - let highScore = parseInt(localStorage.getItem("ee-snake-high-score")); + this.events.on("query_high_score", async () => { + let highScore = parseInt(await getStorageAdapter().get("ee-snake-high-score")); if(isNaN(highScore)) { highScore = 0; } diff --git a/shared/js/ui/modal/css-editor/Controller.ts b/shared/js/ui/modal/css-editor/Controller.ts index c1a6e583..11d27bb8 100644 --- a/shared/js/ui/modal/css-editor/Controller.ts +++ b/shared/js/ui/modal/css-editor/Controller.ts @@ -5,6 +5,8 @@ import {Registry} from "../../../events"; import {LogCategory, logWarn} from "../../../log"; import {tr} from "tc-shared/i18n/localize"; import {spawnModal} from "tc-shared/ui/react-elements/modal"; +import {getStorageAdapter} from "tc-shared/StorageAdapter"; +import {ignorePromise} from "tc-shared/proto"; interface CustomVariable { name: string; @@ -16,15 +18,17 @@ class CssVariableManager { private customVariables: { [key: string]: CustomVariable } = {}; private htmlTag: HTMLStyleElement; - private loadLocalStorage() { + private async loadLocalStorage() { try { - const payloadString = localStorage.getItem("css-custom-variables"); - if (typeof payloadString === "undefined" || !payloadString) + const payloadString = await getStorageAdapter().get("css-custom-variables"); + if (typeof payloadString === "undefined" || !payloadString) { return; + } const payload = JSON.parse(payloadString); - if (payload.version !== 1) + if (payload.version !== 1) { throw "invalid payload version"; + } this.customVariables = payload["customVariables"]; } catch (error) { @@ -32,11 +36,11 @@ class CssVariableManager { } } - initialize() { + async initialize() { this.htmlTag = document.createElement("style"); document.body.appendChild(this.htmlTag); - this.loadLocalStorage(); + await this.loadLocalStorage(); this.updateCustomVariables(false); } @@ -151,17 +155,25 @@ class CssVariableManager { } private updateCustomVariables(updateConfig: boolean) { - let text = "html:root {\n"; - for (const variable of Object.values(this.customVariables)) - text += " " + variable.name + ": " + variable.value + ";\n"; - text += "}"; - this.htmlTag.textContent = text; + const variables = Object.values(this.customVariables); + if(variables.length === 0) { + this.htmlTag.textContent = "/* No custom CSS variables defined */"; + } else { + let text = ""; + text += "/* Custom set CSS variables */\n"; + text = "html:root {\n"; + for (const variable of variables) { + text += " " + variable.name + ": " + variable.value + ";\n"; + } + text += "}"; + this.htmlTag.textContent = text; + } if (updateConfig) { - localStorage.setItem("css-custom-variables", JSON.stringify({ + ignorePromise(getStorageAdapter().set("css-custom-variables", JSON.stringify({ version: 1, customVariables: this.customVariables - })); + }))); } } } @@ -173,7 +185,7 @@ export function spawnModalCssVariableEditor() { cssVariableEditorController(events); const modal = spawnModal("css-editor", [ events.generateIpcDescription() ], { popedOut: true }); - modal.show(); + ignorePromise(modal.show()); } function cssVariableEditorController(events: Registry) { @@ -227,6 +239,6 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { name: "CSS Variable setup", function: async () => { cssVariableManager = new CssVariableManager(); - cssVariableManager.initialize(); + await cssVariableManager.initialize(); } }); \ No newline at end of file diff --git a/shared/js/ui/modal/whats-new/Renderer.tsx b/shared/js/ui/modal/whats-new/Renderer.tsx index ece689e7..4e3534d0 100644 --- a/shared/js/ui/modal/whats-new/Renderer.tsx +++ b/shared/js/ui/modal/whats-new/Renderer.tsx @@ -150,7 +150,7 @@ export const WhatsNew = (props: { changesUI?: ChangeLog, changesClient?: ChangeL ); infoText = ( {versionUIDate} diff --git a/shared/js/update/Updater.ts b/shared/js/update/Updater.ts index f20f8c5f..cf00e141 100644 --- a/shared/js/update/Updater.ts +++ b/shared/js/update/Updater.ts @@ -4,7 +4,10 @@ export interface Updater { getChangeLog() : ChangeLog; getChangeList(oldVersion: string) : ChangeLog; - getLastUsedVersion() : string; + /** + * @returns `undefined` if `updateUsedVersion()` never has been called. + */ + getLastUsedVersion() : string | undefined; getCurrentVersion() : string; /* update the last used version to the current version */ diff --git a/shared/js/update/UpdaterWeb.ts b/shared/js/update/UpdaterWeb.ts index e6431972..08da35c1 100644 --- a/shared/js/update/UpdaterWeb.ts +++ b/shared/js/update/UpdaterWeb.ts @@ -4,6 +4,7 @@ import {Stage} from "tc-loader"; import {setUIUpdater} from "../update/index"; import {Updater} from "../update/Updater"; import {LogCategory, logError} from "../log"; +import {Settings, settings} from "tc-shared/settings"; const ChangeLogContents: string = require("../../../ChangeLog.md"); const EntryRegex = /^\* \*\*([0-9]{2})\.([0-9]{2})\.([0-9]{2})\*\*$/m; @@ -139,11 +140,11 @@ class WebUpdater implements Updater { } getLastUsedVersion(): string { - return localStorage.getItem(kLastUsedVersionKey) || "08.08.20"; + return settings.getValue(Settings.KEY_UPDATER_LAST_USED_UI, undefined); } updateUsedVersion() { - localStorage.setItem(kLastUsedVersionKey, this.getCurrentVersion()); + settings.setValue(Settings.KEY_UPDATER_LAST_USED_UI, this.getCurrentVersion()); } } diff --git a/shared/js/update/index.ts b/shared/js/update/index.ts index ffd14c45..d241d17e 100644 --- a/shared/js/update/index.ts +++ b/shared/js/update/index.ts @@ -3,7 +3,6 @@ import {ChangeLog} from "../update/ChangeLog"; import {spawnUpdatedModal} from "../ui/modal/whats-new/Controller"; import { tr } from "tc-shared/i18n/localize"; -const kIsNewUserKey = "updater-set"; let updaterUi: Updater; let updaterNative: Updater; @@ -22,29 +21,23 @@ export function setNativeUpdater(updater: Updater) { } function getChangedChangeLog(updater: Updater) : ChangeLog | undefined { - if(updater.getCurrentVersion() === updater.getLastUsedVersion()) + if(updater.getCurrentVersion() === updater.getLastUsedVersion()) { return undefined; + } const changes = updater.getChangeList(updater.getLastUsedVersion()); return changes.changes.length > 0 ? changes : undefined; } export function checkForUpdatedApp() { - if(localStorage.getItem(kIsNewUserKey)) { - let changesUI = updaterUi ? getChangedChangeLog(updaterUi) : undefined; - let changesNative = updaterNative ? getChangedChangeLog(updaterNative) : undefined; + let changesUI = updaterUi ? getChangedChangeLog(updaterUi) : undefined; + let changedBackend = updaterNative ? getChangedChangeLog(updaterNative) : undefined; + if(changesUI !== undefined || changedBackend !== undefined) { + spawnUpdatedModal({ + changesUI: changesUI, + changesClient: changedBackend + }); - if(changesUI !== undefined || changesNative !== undefined) { - spawnUpdatedModal({ - changesUI: changesUI, - changesClient: changesNative - }); - - updaterUi?.updateUsedVersion(); - updaterNative?.updateUsedVersion(); - } - } else { - localStorage.setItem(kIsNewUserKey, "1"); updaterUi?.updateUsedVersion(); updaterNative?.updateUsedVersion(); }