Some fixes and minor updates for the TeaClient 1.5.3
parent
fbb40e7ff9
commit
dde13e031b
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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."));
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
})
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue