Some fixes and minor updates for the TeaClient 1.5.3

master
WolverinDEV 2021-04-19 20:26:37 +02:00
parent fbb40e7ff9
commit dde13e031b
18 changed files with 451 additions and 334 deletions

View File

@ -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

View File

@ -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<BookmarkEvents>;
private readonly registeredBookmarks: BookmarkEntry[];
@ -59,25 +60,24 @@ export class BookmarkManager {
this.events = new Registry<BookmarkEvents>();
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

View File

@ -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);
}
autoReconnect = true;
break;
@ -673,6 +657,7 @@ export class ConnectionHandler {
tr("Connection lost"),
tr("Lost connection to remote host (Ping timeout)<br>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<WhisperSessionInitializeData> {

138
shared/js/ServerSettings.ts Normal file
View File

@ -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<V extends RegistryValueType, DV>(key: RegistryKey<V>, defaultValue: DV) : V | DV;
getValue<V extends RegistryValueType>(key: ValuedRegistryKey<V>, 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<T extends RegistryValueType>(key: RegistryKey<T>, 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;
}

View File

@ -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<boolean>;
get(key: string) : Promise<string | null>;
set(key: string, value: string) : Promise<void>;
delete(key: string) : Promise<void>;
}
class LocalStorageAdapter implements StorageAdapter {
async delete(key: string): Promise<void> {
localStorage.removeItem(key);
}
async get(key: string): Promise<string | null> {
return localStorage.getItem(key);
}
async has(key: string): Promise<boolean> {
return localStorage.getItem(key) !== null;
}
async set(key: string, value: string): Promise<void> {
localStorage.setItem(key, value);
}
}
let instance: StorageAdapter = new LocalStorageAdapter();
export function getStorageAdapter() : StorageAdapter {
return instance;
}
export function setStorageAdapter(adapter: StorageAdapter) {
instance = adapter;
}

View File

@ -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."));

View File

@ -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 => {

View File

@ -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() {

View File

@ -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,9 +60,10 @@ 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) {
try {
@ -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<any>((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<any>((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<void> {
if(!logged_in())
if(!logged_in()) {
return;
}
let response;
try {
response = await new Promise<any>((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<void> {
}
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<void> {
}
}
_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);

View File

@ -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 = $;
require("jsrender")($);
}
declare global {
function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;

View File

@ -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 {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<T extends RegistryValueType>(input: string, type:
}
}
function encodeValueToString<T extends RegistryValueType>(input: T) : string {
export function encodeSettingValueToString<T extends RegistryValueType>(input: T) : string {
switch (typeof input) {
case "string":
return input;
@ -73,7 +79,7 @@ function encodeValueToString<T extends RegistryValueType>(input: T) : string {
}
}
function resolveKey<ValueType extends RegistryValueType, DefaultType>(
export function resolveSettingKey<ValueType extends RegistryValueType, DefaultType>(
key: RegistryKey<ValueType>,
resolver: (key: string) => string | undefined | null,
defaultValue: DefaultType
@ -136,9 +142,9 @@ export class UrlParameterParser {
getValue<V extends RegistryValueType>(key: ValuedRegistryKey<V>, defaultValue?: V) : V;
getValue<V extends RegistryValueType, DV>(key: RegistryKey<V> | ValuedRegistryKey<V>, 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<V extends RegistryValueType, DV>(key: RegistryKey<V>, defaultValue: DV) : V | DV;
static<V extends RegistryValueType>(key: ValuedRegistryKey<V>, defaultValue?: V) : V;
static<V extends RegistryValueType, DV>(key: RegistryKey<V> | ValuedRegistryKey<V>, defaultValue: DV) : V | DV {
if(arguments.length > 1) {
return AppParameters.getValue(key, defaultValue);
} else {
return AppParameters.getValue(key as ValuedRegistryKey<V>);
}
}
}
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<string> = {
key: "updater_last_used_ui",
valueType: "string",
description: "Last used TeaSpeak UI version",
}
static readonly KEY_UPDATER_LAST_USED_CLIENT: RegistryKey<string> = {
key: "updater_last_used_client",
valueType: "string",
description: "Last used TeaSpeak Client version (TeaClient only)",
}
static readonly FN_LOG_ENABLED: (category: string) => RegistryKey<boolean> = 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<SettingsEvents>;
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<SettingsEvents>();
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<V extends RegistryValueType>(key: ValuedRegistryKey<V>, defaultValue?: V) : V;
getValue<V extends RegistryValueType, DV>(key: RegistryKey<V> | ValuedRegistryKey<V>, 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<V extends RegistryValueType, DV>(key: RegistryKey<V>, defaultValue: DV) : V | DV;
getValue<V extends RegistryValueType>(key: ValuedRegistryKey<V>, 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<T extends RegistryValueType>(key: RegistryKey<T>, 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);
await getStorageAdapter().set("settings.global", JSON.stringify(this.settingsCache));
} catch (error) {
logError(LogCategory.GENERAL, tr("Failed to load server settings for server %s!\nJson: %s\nError: %o"), server_unique_id, json, error);
}
if(!this.cacheServer)
this.cacheServer = {};
logError(LogCategory.GENERAL, tr("Failed to save global settings: %o"), error);
}
} 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);
default:
case "none":
this.saveState = "saving";
break;
}
if(localStorage.save) {
localStorage.save();
}
}
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;
}
})

View File

@ -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;
}

View File

@ -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<ModalAboutEvents>;
readonly variables: IpcUiVariableProvider<ModalAboutVariables>;
@ -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;
}

View File

@ -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))
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<CssEditorEvents>) {
@ -227,6 +239,6 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "CSS Variable setup",
function: async () => {
cssVariableManager = new CssVariableManager();
cssVariableManager.initialize();
await cssVariableManager.initialize();
}
});

View File

@ -150,7 +150,7 @@ export const WhatsNew = (props: { changesUI?: ChangeLog, changesClient?: ChangeL
);
infoText = (
<VariadicTranslatable key={"info-native-ui"}
text={"The native clients UI has been updated to the version from 18.08.2020."}
text={"The native clients UI has been updated to the version from {}."}
>
{versionUIDate}
</VariadicTranslatable>

View File

@ -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 */

View File

@ -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());
}
}

View File

@ -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;
if(changesUI !== undefined || changesNative !== undefined) {
let changedBackend = updaterNative ? getChangedChangeLog(updaterNative) : undefined;
if(changesUI !== undefined || changedBackend !== undefined) {
spawnUpdatedModal({
changesUI: changesUI,
changesClient: changesNative
changesClient: changedBackend
});
updaterUi?.updateUsedVersion();
updaterNative?.updateUsedVersion();
}
} else {
localStorage.setItem(kIsNewUserKey, "1");
updaterUi?.updateUsedVersion();
updaterNative?.updateUsedVersion();
}