TeaWeb/shared/js/main.tsx

369 lines
14 KiB
TypeScript
Raw Normal View History

2020-03-30 11:44:18 +00:00
import * as loader from "tc-loader";
import * as bipc from "./ipc/BrowserIPC";
2020-03-30 11:44:18 +00:00
import * as sound from "./sound/Sounds";
import * as i18n from "./i18n/localize";
2021-01-10 15:26:45 +00:00
import * as fidentity from "./profiles/identities/TeaForumIdentity";
import * as aplayer from "tc-backend/audio/player";
import * as ppt from "tc-backend/ppt";
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
import {Stage} from "tc-loader";
import {AppParameters, settings, Settings} from "tc-shared/settings";
import {LogCategory, logError, logInfo} from "tc-shared/log";
import {tra} from "./i18n/localize";
2020-09-24 13:51:22 +00:00
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
2020-03-30 11:44:18 +00:00
import {createInfoModal} from "tc-shared/ui/elements/Modal";
2020-09-07 10:42:00 +00:00
import {defaultRecorder, RecorderProfile, setDefaultRecorder} from "tc-shared/voice/RecorderProfile";
2020-03-30 11:44:18 +00:00
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {openModalNewcomer} from "tc-shared/ui/modal/ModalNewcomer";
2020-04-21 14:17:21 +00:00
import {global_client_actions} from "tc-shared/events/GlobalEvents";
import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
2021-01-10 15:13:15 +00:00
import {copyToClipboard} from "tc-shared/utils/helpers";
2020-08-23 08:48:15 +00:00
import {checkForUpdatedApp} from "tc-shared/update";
import {setupJSRender} from "tc-shared/ui/jsrender";
import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
2021-01-10 15:26:45 +00:00
import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile";
import {server_connections} from "tc-shared/ConnectionManager";
import {spawnConnectModalNew} from "tc-shared/ui/modal/connect/Controller";
2020-04-06 14:28:15 +00:00
2020-03-30 11:44:18 +00:00
/* required import for init */
2021-01-10 15:26:45 +00:00
import "svg-sprites/client-icons";
import "../css/load-css"
2020-07-19 16:49:00 +00:00
import "./proto";
2020-08-08 13:20:32 +00:00
import "./video-viewer/Controller";
import "./profiles/ConnectionProfile";
import "./update/UpdaterWeb";
2020-09-25 23:22:21 +00:00
import "./file/LocalIcons";
2021-01-10 15:26:45 +00:00
import "./connection/CommandHandler";
import "./connection/ConnectionBase";
import "./connection/rtc/Connection";
import "./connection/rtc/video/Connection";
import "./video/VideoSource";
import "./media/Video";
2021-01-05 17:13:57 +00:00
import "./ui/AppController";
2021-01-10 15:26:45 +00:00
import "./ui/frames/menu-bar/MainMenu";
2021-01-09 13:25:11 +00:00
import "./ui/modal/connect/Controller";
2021-01-10 15:26:45 +00:00
import "./ui/elements/ContextDivider";
import "./ui/elements/Tab";
import "./clientservice";
2021-01-10 15:26:45 +00:00
import {initializeKeyControl} from "./KeyControl";
import {assertMainApplication} from "tc-shared/ui/utils";
assertMainApplication();
2021-01-09 13:25:11 +00:00
2020-09-16 18:37:39 +00:00
let preventWelcomeUI = false;
async function initialize() {
try {
await i18n.initialize();
} catch(error) {
console.error(tr("Failed to initialized the translation system!\nError: %o"), error);
2019-08-30 21:06:39 +00:00
loader.critical_error("Failed to setup the translation system");
return;
}
2019-04-04 19:47:52 +00:00
bipc.setup();
}
2021-01-10 15:26:45 +00:00
async function initializeApp() {
global_ev_handler.initialize(global_client_actions);
2019-04-04 19:47:52 +00:00
2021-01-05 17:13:57 +00:00
if(!aplayer.initialize()) {
2019-04-04 19:47:52 +00:00
console.warn(tr("Failed to initialize audio controller!"));
2021-01-05 17:13:57 +00:00
}
2021-01-10 15:13:15 +00:00
aplayer.on_ready(() => aplayer.set_master_volume(settings.getValue(Settings.KEY_SOUND_MASTER) / 100));
2019-04-04 19:47:52 +00:00
const recorder = new RecorderProfile("default");
try {
await recorder.initialize();
} catch (error) {
/* TODO: Recover into a defined state? */
2021-01-10 15:26:45 +00:00
logError(LogCategory.AUDIO, tr("Failed to initialize default recorder: %o"), error);
}
setDefaultRecorder(recorder);
2019-04-04 19:47:52 +00:00
sound.initialize().then(() => {
2021-01-10 15:26:45 +00:00
logInfo(LogCategory.AUDIO, tr("Sounds initialized"));
2019-04-04 19:47:52 +00:00
});
2021-01-10 15:13:15 +00:00
sound.set_master_volume(settings.getValue(Settings.KEY_SOUND_MASTER_SOUNDS) / 100);
2019-04-04 19:47:52 +00:00
try {
await ppt.initialize();
} catch(error) {
2021-01-10 15:26:45 +00:00
logError(LogCategory.GENERAL, tr("Failed to initialize ppt!\nError: %o"), error);
2019-08-30 21:06:39 +00:00
loader.critical_error(tr("Failed to initialize ppt!"));
return;
}
}
/* Used by the native client... We can't refactor this yet */
export function handle_connect_request(properties: ConnectRequestData, connection: ConnectionHandler) {
2021-01-10 15:26:45 +00:00
const profile = findConnectProfile(properties.profile) || defaultConnectProfile();
const username = properties.username || profile.connectUsername();
const password = properties.password ? properties.password.value : "";
const password_hashed = properties.password ? properties.password.hashed : false;
if(profile && profile.valid()) {
2021-01-10 15:13:15 +00:00
settings.setValue(Settings.KEY_USER_IS_NEW, false);
if(!aplayer.initialized()) {
2021-01-10 15:26:45 +00:00
/* Trick the client into clicking somewhere on the site */
spawnYesNo(tra("Connect to {}", properties.address), tra("Would you like to connect to {}?", properties.address), result => {
if(result) {
aplayer.on_ready(() => handle_connect_request(properties, connection));
} else {
/* Well... the client don't want to... */
}
}).open();
return;
}
2021-01-10 15:26:45 +00:00
connection.startConnection(properties.address, profile, true, {
nickname: username,
password: password.length > 0 ? {
password: password,
hashed: password_hashed
} : undefined
});
2021-01-10 15:26:45 +00:00
server_connections.setActiveConnectionHandler(connection);
} else {
2021-01-10 13:21:38 +00:00
spawnConnectModalNew({
selectedAddress: properties.address,
selectedProfile: profile
});
2019-09-18 23:25:57 +00:00
}
}
function main() {
2019-08-21 08:00:01 +00:00
/* initialize font */
{
2021-01-10 15:13:15 +00:00
const font = settings.getValue(Settings.KEY_FONT_SIZE);
2021-01-10 15:26:45 +00:00
document.body.style.fontSize = font + "px";
2020-09-17 21:06:02 +00:00
settings.globalChangeListener(Settings.KEY_FONT_SIZE, value => {
2021-01-10 15:26:45 +00:00
document.body.style.fontSize = value + "px";
});
2019-08-21 08:00:01 +00:00
}
/* context menu prevent */
2021-01-10 15:26:45 +00:00
document.addEventListener("contextmenu", event => {
if(event.defaultPrevented) {
2019-08-21 08:00:01 +00:00
return;
}
2019-08-21 08:00:01 +00:00
if(event.target instanceof HTMLInputElement) {
2021-01-10 15:26:45 +00:00
const target = event.target;
if((!!event.target.value || __build.target === "client") && !event.target.disabled && !event.target.readOnly && event.target.type !== "number") {
spawn_context_menu(event.pageX, event.pageY, {
type: MenuEntryType.ENTRY,
name: tr("Copy"),
2021-01-10 15:26:45 +00:00
callback: () => copyToClipboard(target.value),
icon_class: "client-copy",
visible: !!event.target.value
}, {
type: MenuEntryType.ENTRY,
name: tr("Paste"),
callback: () => {
const { clipboard } = __non_webpack_require__('electron');
2021-01-10 15:26:45 +00:00
target.value = clipboard.readText();
},
icon_class: "client-copy",
visible: __build.target === "client",
});
}
2021-01-10 15:13:15 +00:00
event.preventDefault();
return;
}
2021-01-10 15:13:15 +00:00
if(settings.getValue(Settings.KEY_DISABLE_GLOBAL_CONTEXT_MENU)) {
2019-08-21 08:00:01 +00:00
event.preventDefault();
}
2019-08-21 08:00:01 +00:00
});
window.removeLoaderContextMenuHook();
2019-08-21 08:00:01 +00:00
2021-01-10 15:26:45 +00:00
const initialHandler = server_connections.spawnConnectionHandler();
server_connections.setActiveConnectionHandler(initialHandler);
2021-01-10 15:13:15 +00:00
initialHandler.acquireInputHardware().then(() => {});
2020-09-24 13:51:22 +00:00
2018-04-11 15:56:09 +00:00
/** Setup the XF forum identity **/
2020-03-30 11:44:18 +00:00
fidentity.update_forum();
2021-01-10 15:26:45 +00:00
initializeKeyControl();
2019-02-25 14:59:42 +00:00
checkForUpdatedApp();
2019-09-18 23:25:57 +00:00
2021-01-10 15:13:15 +00:00
if(settings.getValue(Settings.KEY_USER_IS_NEW) && !preventWelcomeUI) {
2020-09-16 18:37:39 +00:00
const modal = openModalNewcomer();
2021-01-10 15:13:15 +00:00
modal.close_listener.push(() => settings.setValue(Settings.KEY_USER_IS_NEW, false));
2020-03-27 15:15:15 +00:00
}
2018-04-19 16:42:34 +00:00
}
2019-04-04 19:47:52 +00:00
const task_teaweb_starter: loader.Task = {
name: "voice app starter",
2018-12-30 00:38:13 +00:00
function: async () => {
try {
2021-01-10 15:26:45 +00:00
await initializeApp();
2018-12-30 00:38:13 +00:00
main();
2020-03-30 11:44:18 +00:00
if(!aplayer.initialized()) {
2021-01-10 15:26:45 +00:00
logInfo(LogCategory.VOICE, tr("Initialize audio controller later!"));
2020-03-30 11:44:18 +00:00
if(!aplayer.initializeFromGesture) {
console.error(tr("Missing aplayer.initializeFromGesture"));
2021-01-10 15:13:15 +00:00
} else {
$(document).one('click', () => aplayer.initializeFromGesture());
2021-01-10 15:13:15 +00:00
}
2018-12-30 00:38:13 +00:00
}
2021-01-10 15:13:15 +00:00
loader.config.abortAnimationOnFinish = settings.getValue(Settings.KEY_LOADER_ANIMATION_ABORT);
2018-12-30 00:38:13 +00:00
} catch (ex) {
console.error(ex.stack);
2021-01-10 15:13:15 +00:00
if(ex instanceof ReferenceError || ex instanceof TypeError) {
2018-12-30 00:38:13 +00:00
ex = ex.name + ": " + ex.message;
2021-01-10 15:13:15 +00:00
}
2019-08-30 21:06:39 +00:00
loader.critical_error("Failed to invoke main function:<br>" + ex);
2018-09-25 15:39:38 +00:00
}
2018-12-30 00:38:13 +00:00
},
priority: 10
2019-04-04 19:47:52 +00:00
};
const task_connect_handler: loader.Task = {
name: "Connect handler",
function: async () => {
2021-01-10 15:13:15 +00:00
const address = AppParameters.getValue(AppParameters.KEY_CONNECT_ADDRESS, undefined);
if(typeof address === "undefined") {
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
return;
}
2021-01-10 15:13:15 +00:00
/* FIXME: All additional parameters! */
const connectData = {
address: address,
2021-01-10 15:13:15 +00:00
profile: AppParameters.getValue(AppParameters.KEY_CONNECT_PROFILE, ""),
username: AppParameters.getValue(AppParameters.KEY_CONNECT_NICKNAME, ""),
password: {
value: AppParameters.getValue(AppParameters.KEY_CONNECT_SERVER_PASSWORD, ""),
hashed: true
}
2021-01-10 15:13:15 +00:00
};
2021-01-10 15:13:15 +00:00
const chandler = bipc.getInstanceConnectHandler();
if(chandler && AppParameters.getValue(AppParameters.KEY_CONNECT_NO_SINGLE_INSTANCE)) {
try {
await chandler.post_connect_request(connectData, () => new Promise<boolean>(resolve => {
spawnYesNo(tr("Another TeaWeb instance is already running"), tra("Another TeaWeb instance is already running.{:br:}Would you like to connect there?"), response => {
resolve(response);
}, {
closeable: false
}).open();
}));
2021-01-10 15:26:45 +00:00
logInfo(LogCategory.CLIENT, tr("Executed connect successfully in another browser window. Closing this window"));
2021-01-10 15:13:15 +00:00
const message =
"You're connecting to {0} within the other TeaWeb instance.{:br:}" +
"You could now close this page.";
createInfoModal(
tr("Connecting successfully within other instance"),
formatMessage(/* @tr-ignore */ tr(message), connectData.address),
{
closeable: false,
footer: undefined
}
).open();
return;
} catch(error) {
2021-01-10 15:26:45 +00:00
logInfo(LogCategory.CLIENT, tr("Failed to execute connect within other TeaWeb instance. Using this one. Error: %o"), error);
2021-01-10 15:13:15 +00:00
}
if(chandler) {
/* no instance avail, so lets make us avail */
chandler.callback_available = () => {
return !settings.getValue(Settings.KEY_DISABLE_MULTI_SESSION);
};
chandler.callback_execute = data => {
preventWelcomeUI = true;
2021-01-10 15:26:45 +00:00
handle_connect_request(data, server_connections.spawnConnectionHandler());
2021-01-10 15:13:15 +00:00
return true;
}
}
}
2021-01-10 15:13:15 +00:00
preventWelcomeUI = true;
loader.register_task(loader.Stage.LOADED, {
priority: 0,
2021-01-10 15:26:45 +00:00
function: async () => handle_connect_request(connectData, server_connections.getActiveConnectionHandler() || server_connections.spawnConnectionHandler()),
2021-01-10 15:13:15 +00:00
name: tr("default url connect")
});
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
2019-04-04 19:47:52 +00:00
},
priority: 10
};
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "jrendere initialize",
function: async () => {
try {
2020-08-23 08:48:15 +00:00
if(!setupJSRender())
throw "invalid load";
} catch (error) {
2019-08-30 21:06:39 +00:00
loader.critical_error(tr("Failed to setup jsrender"));
console.error(tr("Failed to load jsrender! %o"), error);
return;
}
},
2020-09-24 13:51:22 +00:00
priority: 110
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
2019-04-04 19:47:52 +00:00
name: "app starter",
function: async () => {
try {
await initialize();
if(__build.target == "web") {
2021-01-10 15:13:15 +00:00
loader.register_task(loader.Stage.LOADED, task_connect_handler);
} else {
2019-04-04 19:47:52 +00:00
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
}
2019-04-04 19:47:52 +00:00
} catch (ex) {
2021-01-10 15:13:15 +00:00
if(ex instanceof Error || typeof(ex.stack) !== "undefined") {
console.error((tr || (msg => msg))("Critical error stack trace: %o"), ex.stack);
2021-01-10 15:13:15 +00:00
}
2021-01-10 15:13:15 +00:00
if(ex instanceof ReferenceError || ex instanceof TypeError) {
2019-04-04 19:47:52 +00:00
ex = ex.name + ": " + ex.message;
2021-01-10 15:13:15 +00:00
}
2019-08-30 21:06:39 +00:00
loader.critical_error("Failed to boot app function:<br>" + ex);
2019-04-04 19:47:52 +00:00
}
},
priority: 1000
2018-10-06 13:13:45 +00:00
});
2018-12-30 00:38:13 +00:00
2020-03-30 11:44:18 +00:00
loader.register_task(loader.Stage.LOADED, {
name: "error task",
function: async () => {
2021-01-10 15:13:15 +00:00
if(AppParameters.getValue(AppParameters.KEY_LOAD_DUMMY_ERROR)) {
loader.critical_error("The tea is cold!", "Argh, this is evil! Cold tea does not taste good.");
2020-03-30 11:44:18 +00:00
throw "The tea is cold!";
}
},
2021-01-10 15:13:15 +00:00
priority: 2000
2020-09-24 13:51:22 +00:00
});
2021-01-10 15:26:45 +00:00
/* TODO: Remove this after the image preview has been rewritten into react */
2020-09-24 13:51:22 +00:00
loader.register_task(Stage.JAVASCRIPT_INITIALIZING,{
name: "app init",
function: async () => {
2021-01-10 15:26:45 +00:00
try {
$("body").append($("#tmpl_main").renderTag());
2020-09-24 13:51:22 +00:00
} catch(error) {
2021-01-10 15:26:45 +00:00
logError(LogCategory.GENERAL, error);
2020-09-24 13:51:22 +00:00
loader.critical_error(tr("Failed to setup main page!"));
return;
}
},
priority: 100
2020-04-01 13:40:45 +00:00
});