From aa7b9fff4395e9717d346a2ea7a28d65d4d306ba Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Sat, 22 Aug 2020 17:50:38 +0200 Subject: [PATCH] Some minimal client improvements --- shared/js/ConnectionHandler.ts | 1 + shared/js/connection/CommandHandler.ts | 4 +- shared/js/connection/ErrorCode.ts | 177 ++++++++ .../connection/ServerConnectionDeclaration.ts | 52 +-- shared/js/main.tsx | 54 --- shared/js/profiles/ConnectionProfile.ts | 7 +- shared/js/ui/client.ts | 6 +- .../js/ui/frames/side/AbstractConversion.ts | 392 ++++++++++++++++++ .../js/ui/frames/side/ConversationManager.ts | 386 +---------------- .../frames/side/PrivateConversationManager.ts | 2 +- web/app/UnloadHandler.ts | 12 + web/app/index.ts | 2 + 12 files changed, 601 insertions(+), 494 deletions(-) create mode 100644 shared/js/connection/ErrorCode.ts create mode 100644 shared/js/ui/frames/side/AbstractConversion.ts create mode 100644 web/app/UnloadHandler.ts diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 64f86f47..961944e3 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -850,6 +850,7 @@ export class ConnectionHandler { this.update_voice_status(); } catch (error) { this.setInputHardwareState(InputHardwareState.START_FAILED); + this.update_voice_status(); let errorMessage; if(error === InputStartResult.ENOTSUPPORTED) { diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index 17480a71..70213df3 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -113,14 +113,14 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { if(ex instanceof CommandResult) { let res = ex; if(!res.success) { - if(res.id == ErrorID.PERMISSION_ERROR) { //Permission error + if(res.id == ErrorID.SERVER_INSUFFICIENT_PERMISSIONS) { //Permission error const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number); res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown"); this.connection_handler.log.log(EventType.ERROR_PERMISSION, { permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number) }); this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); - } else if(res.id != ErrorID.EMPTY_RESULT) { + } else if(res.id != ErrorID.DATABASE_EMPTY_RESULT) { this.connection_handler.log.log(EventType.ERROR_CUSTOM, { message: res.extra_message.length == 0 ? res.message : res.extra_message }); diff --git a/shared/js/connection/ErrorCode.ts b/shared/js/connection/ErrorCode.ts new file mode 100644 index 00000000..1b49c186 --- /dev/null +++ b/shared/js/connection/ErrorCode.ts @@ -0,0 +1,177 @@ +export enum ErrorCode { + OK = 0x0, + UNDEFINED = 0x1, + NOT_IMPLEMENTED = 0x2, + LIB_TIME_LIMIT_REACHED = 0x5, + + COMMAND_NOT_FOUND = 0x100, + UNABLE_TO_BIND_NETWORK_PORT = 0x101, + NO_NETWORK_PORT_AVAILABLE = 0x102, + + /* mainly used by the teaclient */ + COMMAND_TIMED_OUT = 0x110, + COMMAND_ABORTED_CONNECTION_CLOSED = 0x111, + + CLIENT_INVALID_ID = 0x200, + CLIENT_NICKNAME_INUSE = 0x201, + INVALID_ERROR_CODE = 0x202, + + CLIENT_PROTOCOL_LIMIT_REACHED = 0x203, + CLIENT_INVALID_TYPE = 0x204, + CLIENT_ALREADY_SUBSCRIBED = 0x205, + CLIENT_NOT_LOGGED_IN = 0x206, + CLIENT_COULD_NOT_VALIDATE_IDENTITY = 0x207, + CLIENT_INVALID_PASSWORD = 0x208, + CLIENT_TOO_MANY_CLONES_CONNECTED = 0x209, + CLIENT_VERSION_OUTDATED = 0x20A, + CLIENT_IS_ONLINE = 0x20B, + CLIENT_IS_FLOODING = 0x20C, + CLIENT_HACKED = 0x20D, + CLIENT_CANNOT_VERIFY_NOW = 0x20E, + CLIENT_LOGIN_NOT_PERMITTED = 0x20F, + CLIENT_NOT_SUBSCRIBED = 0x210, + CLIENT_UNKNOWN = 0x0211, + CLIENT_JOIN_RATE_LIMIT_REACHED = 0x0212, + CLIENT_IS_ALREADY_MEMBER_OF_GROUP = 0x0213, + CLIENT_IS_NOT_MEMBER_OF_GROUP = 0x0214, + CLIENT_TYPE_IS_NOT_ALLOWED = 0x0215, + + CHANNEL_INVALID_ID = 0x300, + CHANNEL_PROTOCOL_LIMIT_REACHED = 0x301, + CHANNEL_ALREADY_IN = 0x302, + CHANNEL_NAME_INUSE = 0x303, + CHANNEL_NOT_EMPTY = 0x304, + CHANNEL_CAN_NOT_DELETE_DEFAULT = 0x305, + CHANNEL_DEFAULT_REQUIRE_PERMANENT = 0x306, + CHANNEL_INVALID_FLAGS = 0x307, + CHANNEL_PARENT_NOT_PERMANENT = 0x308, + CHANNEL_MAXCLIENTS_REACHED = 0x309, + CHANNEL_MAXFAMILY_REACHED = 0x30A, + CHANNEL_INVALID_ORDER = 0x30B, + CHANNEL_NO_FILETRANSFER_SUPPORTED = 0x30C, + CHANNEL_INVALID_PASSWORD = 0x30D, + CHANNEL_IS_PRIVATE_CHANNEL = 0x30E, + CHANNEL_INVALID_SECURITY_HASH = 0x30F, + CHANNEL_IS_DELETED = 0x310, + CHANNEL_NAME_INVALID = 0x311, + CHANNEL_LIMIT_REACHED = 0x312, + + SERVER_INVALID_ID = 0x400, + SERVER_RUNNING = 0x401, + SERVER_IS_SHUTTING_DOWN = 0x402, + SERVER_MAXCLIENTS_REACHED = 0x403, + SERVER_INVALID_PASSWORD = 0x404, + SERVER_DEPLOYMENT_ACTIVE = 0x405, + SERVER_UNABLE_TO_STOP_OWN_SERVER = 0x406, + SERVER_IS_VIRTUAL = 0x407, + SERVER_WRONG_MACHINEID = 0x408, + SERVER_IS_NOT_RUNNING = 0x409, + SERVER_IS_BOOTING = 0x40A, + SERVER_STATUS_INVALID = 0x40B, + SERVER_MODAL_QUIT = 0x40C, + SERVER_VERSION_OUTDATED = 0x40D, + SERVER_ALREADY_JOINED = 0x40D, + SERVER_IS_NOT_SHUTTING_DOWN = 0x40E, + SERVER_MAX_VS_REACHED = 0x40F, + SERVER_UNBOUND = 0x410, + SERVER_JOIN_RATE_LIMIT_REACHED = 0x411, + + SQL = 0x500, + DATABASE_EMPTY_RESULT = 0x501, + DATABASE_DUPLICATE_ENTRY = 0x502, + DATABASE_NO_MODIFICATIONS = 0x503, + DATABASE_CONSTRAINT = 0x504, + DATABASE_REINVOKE = 0x505, + + PARAMETER_QUOTE = 0x600, + PARAMETER_INVALID_COUNT = 0x601, + PARAMETER_INVALID = 0x602, + PARAMETER_NOT_FOUND = 0x603, + PARAMETER_CONVERT = 0x604, + PARAMETER_INVALID_SIZE = 0x605, + PARAMETER_MISSING = 0x606, + PARAMETER_CHECKSUM = 0x607, + PARAMETER_CONSTRAINT_VIOLATION = 0x6010, + + VS_CRITICAL = 0x700, + CONNECTION_LOST = 0x701, + NOT_CONNECTED = 0x702, + NO_CACHED_CONNECTION_INFO = 0x703, + CURRENTLY_NOT_POSSIBLE = 0x704, + FAILED_CONNECTION_INITIALISATION = 0x705, + COULD_NOT_RESOLVE_HOSTNAME = 0x706, + INVALID_SERVER_CONNECTION_HANDLER_ID = 0x707, + COULD_NOT_INITIALISE_INPUT_CLIENT = 0x708, + CLIENTLIBRARY_NOT_INITIALISED = 0x709, + SERVERLIBRARY_NOT_INITIALISED = 0x70A, + WHISPER_TOO_MANY_TARGETS = 0x70B, + WHISPER_NO_TARGETS = 0x70C, + + FILE_INVALID_NAME = 0x800, + FILE_INVALID_PERMISSIONS = 0x801, + FILE_ALREADY_EXISTS = 0x802, + FILE_NOT_FOUND = 0x803, + FILE_IO_ERROR = 0x804, + FILE_INVALID_TRANSFER_ID = 0x805, + FILE_INVALID_PATH = 0x806, + FILE_NO_FILES_AVAILABLE = 0x807, + FILE_OVERWRITE_EXCLUDES_RESUME = 0x808, + FILE_INVALID_SIZE = 0x809, + FILE_ALREADY_IN_USE = 0x80A, + FILE_COULD_NOT_OPEN_CONNECTION = 0x80B, + FILE_NO_SPACE_LEFT_ON_DEVICE = 0x80C, + FILE_EXCEEDS_FILE_SYSTEM_MAXIMUM_SIZE = 0x80D, + FILE_TRANSFER_CONNECTION_TIMEOUT = 0x80E, + FILE_CONNECTION_LOST = 0x80F, + FILE_EXCEEDS_SUPPLIED_SIZE = 0x810, + FILE_TRANSFER_COMPLETE = 0x811, + FILE_TRANSFER_CANCELED = 0x812, + FILE_TRANSFER_INTERRUPTED = 0x813, + FILE_TRANSFER_SERVER_QUOTA_EXCEEDED = 0x814, + FILE_TRANSFER_CLIENT_QUOTA_EXCEEDED = 0x815, + FILE_TRANSFER_RESET = 0x816, + FILE_TRANSFER_LIMIT_REACHED = 0x817, + + FILE_API_TIMEOUT = 0x820, + FILE_VIRTUAL_SERVER_NOT_REGISTERED = 0x821, + FILE_SERVER_TRANSFER_LIMIT_REACHED = 0x822, + FILE_CLIENT_TRANSFER_LIMIT_REACHED = 0x823, + + SERVER_INSUFFICIENT_PERMISSIONS = 0xA08, + ACCOUNTING_SLOT_LIMIT_REACHED = 0xB01, + SERVER_CONNECT_BANNED = 0xD01, + BAN_FLOODING = 0xD03, + TOKEN_INVALID_ID = 0xF00, + WEB_HANDSHAKE_INVALID = 0x1000, + WEB_HANDSHAKE_UNSUPPORTED = 0x1001, + WEB_HANDSHAKE_IDENTITY_UNSUPPORTED = 0x1002, + WEB_HANDSHAKE_IDENTITY_PROOF_FAILED = 0x1003, + WEB_HANDSHAKE_IDENTITY_OUTDATED = 0x1004, + + MUSIC_INVALID_ID = 0x1100, + MUSIC_LIMIT_REACHED = 0x1101, + MUSIC_CLIENT_LIMIT_REACHED = 0x1102, + MUSIC_INVALID_PLAYER_STATE = 0x1103, + MUSIC_INVALID_ACTION = 0x1104, + MUSIC_NO_PLAYER = 0x1105, + MUSIC_DISABLED = 0x1105, + PLAYLIST_INVALID_ID = 0x2100, + PLAYLIST_INVALID_SONG_ID = 0x2101, + PLAYLIST_ALREADY_IN_USE = 0x2102, + PLAYLIST_IS_IN_USE = 0x2103, + QUERY_NOT_EXISTS = 0x1200, + QUERY_ALREADY_EXISTS = 0x1201, + + GROUP_INVALID_ID = 0x1300, + GROUP_NAME_INUSE = 0x1301, + GROUP_NOT_ASSIGNED_OVER_THIS_SERVER = 0x1302, + + CONVERSATION_INVALID_ID = 0x2200, + CONVERSATION_MORE_DATA = 0x2201, + CONVERSATION_IS_PRIVATE = 0x2202, + + CUSTOM_ERROR = 0xFFFF, + + PERMISSION_ERROR = ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS, + EMPTY_RESULT = ErrorCode.DATABASE_EMPTY_RESULT +} \ No newline at end of file diff --git a/shared/js/connection/ServerConnectionDeclaration.ts b/shared/js/connection/ServerConnectionDeclaration.ts index a6060bfe..4596ee68 100644 --- a/shared/js/connection/ServerConnectionDeclaration.ts +++ b/shared/js/connection/ServerConnectionDeclaration.ts @@ -1,54 +1,8 @@ import {LaterPromise} from "tc-shared/utils/LaterPromise"; +import {ErrorCode} from "./ErrorCode"; -export enum ErrorID { - NOT_IMPLEMENTED = 0x2, - COMMAND_NOT_FOUND = 0x100, - - PERMISSION_ERROR = 2568, - EMPTY_RESULT = 0x0501, - PLAYLIST_IS_IN_USE = 0x2103, - - FILE_ALREADY_EXISTS = 2050, - FILE_NOT_FOUND = 2051, - - CLIENT_INVALID_ID = 0x0200, - - CONVERSATION_INVALID_ID = 0x2200, - CONVERSATION_MORE_DATA = 0x2201, - CONVERSATION_IS_PRIVATE = 0x2202 -} - -export enum ErrorCode { - FILE_INVALID_NAME = 0X800, - FILE_INVALID_PERMISSIONS = 0X801, - FILE_ALREADY_EXISTS = 0X802, - FILE_NOT_FOUND = 0X803, - FILE_IO_ERROR = 0X804, - FILE_INVALID_TRANSFER_ID = 0X805, - FILE_INVALID_PATH = 0X806, - FILE_NO_FILES_AVAILABLE = 0X807, - FILE_OVERWRITE_EXCLUDES_RESUME = 0X808, - FILE_INVALID_SIZE = 0X809, - FILE_ALREADY_IN_USE = 0X80A, - FILE_COULD_NOT_OPEN_CONNECTION = 0X80B, - FILE_NO_SPACE_LEFT_ON_DEVICE = 0X80C, - FILE_EXCEEDS_FILE_SYSTEM_MAXIMUM_SIZE = 0X80D, - FILE_TRANSFER_CONNECTION_TIMEOUT = 0X80E, - FILE_CONNECTION_LOST = 0X80F, - FILE_EXCEEDS_SUPPLIED_SIZE = 0X810, - FILE_TRANSFER_COMPLETE = 0X811, - FILE_TRANSFER_CANCELED = 0X812, - FILE_TRANSFER_INTERRUPTED = 0X813, - FILE_TRANSFER_SERVER_QUOTA_EXCEEDED = 0X814, - FILE_TRANSFER_CLIENT_QUOTA_EXCEEDED = 0X815, - FILE_TRANSFER_RESET = 0X816, - FILE_TRANSFER_LIMIT_REACHED = 0X817, - - FILE_API_TIMEOUT = 0X820, - FILE_VIRTUAL_SERVER_NOT_REGISTERED = 0X821, - FILE_SERVER_TRANSFER_LIMIT_REACHED = 0X822, - FILE_CLIENT_TRANSFER_LIMIT_REACHED = 0X823, -} +/* legacy */ +export const ErrorID = ErrorCode; export class CommandResult { success: boolean; diff --git a/shared/js/main.tsx b/shared/js/main.tsx index 749ce17c..2ad872f1 100644 --- a/shared/js/main.tsx +++ b/shared/js/main.tsx @@ -50,58 +50,6 @@ declare global { } } -function setup_close() { - window.onbeforeunload = event => { - if(profiles.requires_save()) - profiles.save(); - - if(!settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG, false)) { - const active_connections = server_connections.all_connections().filter(e => e.connected); - if(active_connections.length == 0) return; - - if(__build.target === "web") { - event.returnValue = "Are you really sure?
You're still connected!"; - } else { - const do_exit = () => { - const dp = server_connections.all_connections().map(e => { - if(e.serverConnection.connected()) - return e.serverConnection.disconnect(tr("client closed")); - return Promise.resolve(); - }).map(e => e.catch(() => { - console.warn(tr("Failed to disconnect from server on client close: %o"), e); - })); - - const exit = () => { - const {remote} = window.require('electron'); - remote.getCurrentWindow().close(); - }; - - Promise.all(dp).then(exit); - /* force exit after 2500ms */ - setTimeout(exit, 2500); - }; - if(window.open_connected_question) { - event.preventDefault(); - event.returnValue = "question"; - window.open_connected_question().then(result => { - if(result) { - /* prevent quitting because we try to disconnect */ - window.onbeforeunload = e => e.preventDefault(); - - /* allow a force quit after 5 seconds */ - setTimeout(() => window.onbeforeunload, 5000); - do_exit(); - } - }); - } else { - /* we're in debugging mode */ - do_exit(); - } - } - } - }; -} - function setup_jsrender() : boolean { if(!$.views) { loader.critical_error("Missing jsrender viewer extension!"); @@ -203,8 +151,6 @@ async function initialize_app() { loader.critical_error(tr("Failed to initialize ppt!")); return; } - - setup_close(); } /* diff --git a/shared/js/profiles/ConnectionProfile.ts b/shared/js/profiles/ConnectionProfile.ts index 0142900e..9fa7c22a 100644 --- a/shared/js/profiles/ConnectionProfile.ts +++ b/shared/js/profiles/ConnectionProfile.ts @@ -254,4 +254,9 @@ export function set_default_profile(profile: ConnectionProfile) { export function delete_profile(profile: ConnectionProfile) { available_profiles.remove(profile); -} \ No newline at end of file +} + +window.addEventListener("beforeunload", event => { + if(requires_save()) + save(); +}); \ No newline at end of file diff --git a/shared/js/ui/client.ts b/shared/js/ui/client.ts index a03d7281..774d76da 100644 --- a/shared/js/ui/client.ts +++ b/shared/js/ui/client.ts @@ -795,15 +795,15 @@ export class ClientEntry extends ChannelTreeEntry { } /* process updates after variables have been set */ - const side_bar = this.channelTree.client.side_bar; - { + const side_bar = this.channelTree?.client?.side_bar; + if(side_bar) { const client_info = side_bar.client_info(); if(client_info.current_client() === this) client_info.set_current_client(this, true); /* force an update */ } if(update_avatar) - this.channelTree.client.fileManager.avatars.updateCache(this.avatarId(), this.properties.client_flag_avatar); + this.channelTree.client?.fileManager?.avatars.updateCache(this.avatarId(), this.properties.client_flag_avatar); /* devel-block(log-client-property-updates) */ group.end(); diff --git a/shared/js/ui/frames/side/AbstractConversion.ts b/shared/js/ui/frames/side/AbstractConversion.ts new file mode 100644 index 00000000..feaee6cb --- /dev/null +++ b/shared/js/ui/frames/side/AbstractConversion.ts @@ -0,0 +1,392 @@ +import { + ChatEvent, + ChatEventMessage, ChatHistoryState, ChatMessage, + ChatState, ConversationHistoryResponse, + ConversationUIEvents +} from "tc-shared/ui/frames/side/ConversationDefinitions"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {EventHandler, Registry} from "tc-shared/events"; +import {preprocessChatMessageForSend} from "tc-shared/text/chat"; +import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {tra} from "tc-shared/i18n/localize"; + +export const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */ + +export abstract class AbstractChat { + protected readonly connection: ConnectionHandler; + protected readonly chatId: string; + protected readonly events: Registry; + protected presentMessages: ChatEvent[] = []; + protected presentEvents: Exclude[] = []; /* everything excluding chat messages */ + + protected mode: ChatState = "unloaded"; + protected failedPermission: string; + protected errorMessage: string; + + protected conversationPrivate: boolean = false; + protected crossChannelChatSupported: boolean = true; + + protected unreadTimestamp: number | undefined = undefined; + protected lastReadMessage: number = 0; + + protected historyErrorMessage: string; + protected historyRetryTimestamp: number = 0; + protected executingUIHistoryQuery = false; + + protected messageSendEnabled: boolean = true; + + protected hasHistory = false; + + protected constructor(connection: ConnectionHandler, chatId: string, events: Registry) { + this.connection = connection; + this.events = events; + this.chatId = chatId; + } + + public currentMode() : ChatState { return this.mode; }; + + protected registerChatEvent(event: ChatEvent, triggerUnread: boolean) { + if(event.type === "message") { + let index = 0; + while(index < this.presentMessages.length && this.presentMessages[index].timestamp <= event.timestamp) + index++; + + this.presentMessages.splice(index, 0, event); + + const deleteMessageCount = Math.max(0, this.presentMessages.length - kMaxChatFrameMessageSize); + this.presentMessages.splice(0, deleteMessageCount); + if(deleteMessageCount > 0) + this.hasHistory = true; + index -= deleteMessageCount; + + if(event.isOwnMessage) + this.setUnreadTimestamp(undefined); + else if(!this.unreadTimestamp) + this.setUnreadTimestamp(event.message.timestamp); + + /* let all other events run before */ + this.events.fire_async("notify_chat_event", { + chatId: this.chatId, + triggerUnread: triggerUnread, + event: event + }); + } else { + this.presentEvents.push(event); + this.presentEvents.sort((a, b) => a.timestamp - b.timestamp); + /* TODO: Cutoff too old events! */ + + this.events.fire("notify_chat_event", { + chatId: this.chatId, + triggerUnread: triggerUnread, + event: event + }); + } + } + + protected registerIncomingMessage(message: ChatMessage, isOwnMessage: boolean, uniqueId: string) { + this.registerChatEvent({ + type: "message", + isOwnMessage: isOwnMessage, + uniqueId: uniqueId, + timestamp: message.timestamp, + message: message + }, !isOwnMessage); + } + + public reportStateToUI() { + let historyState: ChatHistoryState; + if(Date.now() < this.historyRetryTimestamp && this.historyErrorMessage) { + historyState = "error"; + } else if(this.executingUIHistoryQuery) { + historyState = "loading"; + } else if(this.hasHistory) { + historyState = "available"; + } else { + historyState = "none"; + } + + switch (this.mode) { + case "normal": + if(this.conversationPrivate && !this.canClientAccessChat()) { + this.events.fire_async("notify_conversation_state", { + chatId: this.chatId, + state: "private", + crossChannelChatSupported: this.crossChannelChatSupported + }); + return; + } + + this.events.fire_async("notify_conversation_state", { + chatId: this.chatId, + state: "normal", + + historyState: historyState, + historyErrorMessage: this.historyErrorMessage, + historyRetryTimestamp: this.historyRetryTimestamp, + + chatFrameMaxMessageCount: kMaxChatFrameMessageSize, + unreadTimestamp: this.unreadTimestamp, + + showUserSwitchEvents: this.conversationPrivate || !this.crossChannelChatSupported, + sendEnabled: this.messageSendEnabled, + + events: [...this.presentEvents, ...this.presentMessages] + }); + break; + + case "loading": + case "unloaded": + this.events.fire_async("notify_conversation_state", { + chatId: this.chatId, + state: "loading" + }); + break; + + case "error": + this.events.fire_async("notify_conversation_state", { + chatId: this.chatId, + state: "error", + errorMessage: this.errorMessage + }); + break; + + case "no-permissions": + this.events.fire_async("notify_conversation_state", { + chatId: this.chatId, + state: "no-permissions", + failedPermission: this.failedPermission + }); + break; + + } + } + + protected doSendMessage(message: string, targetMode: number, target: number) : Promise { + let msg = preprocessChatMessageForSend(message); + return this.connection.serverConnection.send_command("sendtextmessage", { + targetmode: targetMode, + cid: target, + target: target, + msg: msg + }, { process_result: false }).then(async () => true).catch(error => { + if(error instanceof CommandResult) { + if(error.id === ErrorID.PERMISSION_ERROR) { + this.registerChatEvent({ + type: "message-failed", + uniqueId: "msf-" + this.chatId + "-" + Date.now(), + timestamp: Date.now(), + error: "permission", + failedPermission: this.connection.permissions.resolveInfo(parseInt(error.json["failed_permid"]))?.name || tr("unknown") + }, false); + } else { + this.registerChatEvent({ + type: "message-failed", + uniqueId: "msf-" + this.chatId + "-" + Date.now(), + timestamp: Date.now(), + error: "error", + errorMessage: error.formattedMessage() + }, false); + } + } else if(typeof error === "string") { + this.registerChatEvent({ + type: "message-failed", + uniqueId: "msf-" + this.chatId + "-" + Date.now(), + timestamp: Date.now(), + error: "error", + errorMessage: error + }, false); + } else { + log.warn(LogCategory.CHAT, tr("Failed to send channel chat message to %s: %o"), this.chatId, error); + this.registerChatEvent({ + type: "message-failed", + uniqueId: "msf-" + this.chatId + "-" + Date.now(), + timestamp: Date.now(), + error: "error", + errorMessage: tr("lookup the console") + }, false); + } + return false; + }); + } + + public isUnread() { + return this.unreadTimestamp !== undefined; + } + + public setUnreadTimestamp(timestamp: number | undefined) { + if(timestamp === undefined) + this.lastReadMessage = Date.now(); + + if(this.unreadTimestamp === timestamp) + return; + + this.unreadTimestamp = timestamp; + this.events.fire_async("notify_unread_timestamp_changed", { chatId: this.chatId, timestamp: timestamp }); + } + + public jumpToPresent() { + this.reportStateToUI(); + } + + public uiQueryHistory(timestamp: number, enforce?: boolean) { + if(this.executingUIHistoryQuery && !enforce) + return; + + this.executingUIHistoryQuery = true; + this.queryHistory({ end: 1, begin: timestamp, limit: kMaxChatFrameMessageSize }).then(result => { + this.executingUIHistoryQuery = false; + this.historyErrorMessage = undefined; + this.historyRetryTimestamp = result.nextAllowedQuery; + + switch (result.status) { + case "success": + this.events.fire_async("notify_conversation_history", { + chatId: this.chatId, + state: "success", + + hasMoreMessages: result.moreEvents, + retryTimestamp: this.historyRetryTimestamp, + + events: result.events + }); + break; + + case "private": + this.events.fire_async("notify_conversation_history", { + chatId: this.chatId, + state: "error", + errorMessage: this.historyErrorMessage = tr("chat is private"), + retryTimestamp: this.historyRetryTimestamp + }); + break; + + case "no-permission": + this.events.fire_async("notify_conversation_history", { + chatId: this.chatId, + state: "error", + errorMessage: this.historyErrorMessage = tra("failed on {}", result.failedPermission || tr("unknown permission")), + retryTimestamp: this.historyRetryTimestamp + }); + break; + + case "error": + this.events.fire_async("notify_conversation_history", { + chatId: this.chatId, + state: "error", + errorMessage: this.historyErrorMessage = result.errorMessage, + retryTimestamp: this.historyRetryTimestamp + }); + break; + } + }); + } + + protected lastEvent() : ChatEvent | undefined { + if(this.presentMessages.length === 0) + return this.presentEvents.last(); + else if(this.presentEvents.length === 0 || this.presentMessages.last().timestamp > this.presentEvents.last().timestamp) + return this.presentMessages.last(); + else + return this.presentEvents.last(); + } + + protected sendMessageSendingEnabled(enabled: boolean) { + if(this.messageSendEnabled === enabled) + return; + + this.messageSendEnabled = enabled; + this.events.fire("notify_send_enabled", { chatId: this.chatId, enabled: enabled }); + } + + protected abstract canClientAccessChat() : boolean; + public abstract queryHistory(criteria: { begin?: number, end?: number, limit?: number }) : Promise; + public abstract queryCurrentMessages(); + public abstract sendMessage(text: string); +} + +export abstract class AbstractChatManager { + protected readonly uiEvents: Registry; + + protected constructor() { + this.uiEvents = new Registry(); + } + + handlePanelShow() { + this.uiEvents.fire("notify_panel_show"); + } + + protected abstract findChat(id: string) : AbstractChat; + + @EventHandler("query_conversation_state") + protected handleQueryConversationState(event: ConversationUIEvents["query_conversation_state"]) { + const conversation = this.findChat(event.chatId); + if(!conversation) { + this.uiEvents.fire_async("notify_conversation_state", { + state: "error", + errorMessage: tr("Unknown conversation"), + + chatId: event.chatId + }); + return; + } + + if(conversation.currentMode() === "unloaded") + conversation.queryCurrentMessages(); + else + conversation.reportStateToUI(); + } + + @EventHandler("query_conversation_history") + protected handleQueryHistory(event: ConversationUIEvents["query_conversation_history"]) { + const conversation = this.findChat(event.chatId); + if(!conversation) { + this.uiEvents.fire_async("notify_conversation_history", { + state: "error", + errorMessage: tr("Unknown conversation"), + retryTimestamp: Date.now() + 10 * 1000, + + chatId: event.chatId + }); + + log.error(LogCategory.CLIENT, tr("Tried to query history for an unknown conversation with id %s"), event.chatId); + return; + } + + conversation.uiQueryHistory(event.timestamp); + } + + @EventHandler("action_clear_unread_flag") + protected handleClearUnreadFlag(event: ConversationUIEvents["action_clear_unread_flag"]) { + this.findChat(event.chatId)?.setUnreadTimestamp(undefined); + } + + @EventHandler("action_self_typing") + protected handleActionSelfTyping(event: ConversationUIEvents["action_self_typing"]) { + if(this.findChat(event.chatId)?.isUnread()) + this.uiEvents.fire("action_clear_unread_flag", { chatId: event.chatId }); + } + + @EventHandler("action_send_message") + protected handleSendMessage(event: ConversationUIEvents["action_send_message"]) { + const conversation = this.findChat(event.chatId); + if(!conversation) { + log.error(LogCategory.CLIENT, tr("Tried to send a chat message to an unknown conversation with id %s"), event.chatId); + return; + } + + conversation.sendMessage(event.text); + } + + @EventHandler("action_jump_to_present") + protected handleJumpToPresent(event: ConversationUIEvents["action_jump_to_present"]) { + const conversation = this.findChat(event.chatId); + if(!conversation) { + log.error(LogCategory.CLIENT, tr("Tried to jump to present for an unknown conversation with id %s"), event.chatId); + return; + } + + conversation.jumpToPresent(); + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ConversationManager.ts b/shared/js/ui/frames/side/ConversationManager.ts index a48bd951..e4ec8454 100644 --- a/shared/js/ui/frames/side/ConversationManager.ts +++ b/shared/js/ui/frames/side/ConversationManager.ts @@ -6,397 +6,15 @@ import {LogCategory} from "tc-shared/log"; import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; import {ServerCommand} from "tc-shared/connection/ConnectionBase"; import {Settings} from "tc-shared/settings"; -import {tra, traj} from "tc-shared/i18n/localize"; +import {traj} from "tc-shared/i18n/localize"; import {createErrorModal} from "tc-shared/ui/elements/Modal"; import ReactDOM = require("react-dom"); import { - ChatEvent, - ChatEventMessage, ChatHistoryState, ChatMessage, ConversationHistoryResponse, - ChatState, ConversationUIEvents } from "tc-shared/ui/frames/side/ConversationDefinitions"; import {ConversationPanel} from "tc-shared/ui/frames/side/ConversationUI"; -import {preprocessChatMessageForSend} from "tc-shared/text/chat"; - -const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */ - -export abstract class AbstractChat { - protected readonly connection: ConnectionHandler; - protected readonly chatId: string; - protected readonly events: Registry; - protected presentMessages: ChatEvent[] = []; - protected presentEvents: Exclude[] = []; /* everything excluding chat messages */ - - protected mode: ChatState = "unloaded"; - protected failedPermission: string; - protected errorMessage: string; - - protected conversationPrivate: boolean = false; - protected crossChannelChatSupported: boolean = true; - - protected unreadTimestamp: number | undefined = undefined; - protected lastReadMessage: number = 0; - - protected historyErrorMessage: string; - protected historyRetryTimestamp: number = 0; - protected executingUIHistoryQuery = false; - - protected messageSendEnabled: boolean = true; - - protected hasHistory = false; - - protected constructor(connection: ConnectionHandler, chatId: string, events: Registry) { - this.connection = connection; - this.events = events; - this.chatId = chatId; - } - - public currentMode() : ChatState { return this.mode; }; - - protected registerChatEvent(event: ChatEvent, triggerUnread: boolean) { - if(event.type === "message") { - let index = 0; - while(index < this.presentMessages.length && this.presentMessages[index].timestamp <= event.timestamp) - index++; - - this.presentMessages.splice(index, 0, event); - - const deleteMessageCount = Math.max(0, this.presentMessages.length - kMaxChatFrameMessageSize); - this.presentMessages.splice(0, deleteMessageCount); - if(deleteMessageCount > 0) - this.hasHistory = true; - index -= deleteMessageCount; - - if(event.isOwnMessage) - this.setUnreadTimestamp(undefined); - else if(!this.unreadTimestamp) - this.setUnreadTimestamp(event.message.timestamp); - - /* let all other events run before */ - this.events.fire_async("notify_chat_event", { - chatId: this.chatId, - triggerUnread: triggerUnread, - event: event - }); - } else { - this.presentEvents.push(event); - this.presentEvents.sort((a, b) => a.timestamp - b.timestamp); - /* TODO: Cutoff too old events! */ - - this.events.fire("notify_chat_event", { - chatId: this.chatId, - triggerUnread: triggerUnread, - event: event - }); - } - } - - protected registerIncomingMessage(message: ChatMessage, isOwnMessage: boolean, uniqueId: string) { - this.registerChatEvent({ - type: "message", - isOwnMessage: isOwnMessage, - uniqueId: uniqueId, - timestamp: message.timestamp, - message: message - }, !isOwnMessage); - } - - public reportStateToUI() { - let historyState: ChatHistoryState; - if(Date.now() < this.historyRetryTimestamp && this.historyErrorMessage) { - historyState = "error"; - } else if(this.executingUIHistoryQuery) { - historyState = "loading"; - } else if(this.hasHistory) { - historyState = "available"; - } else { - historyState = "none"; - } - - switch (this.mode) { - case "normal": - if(this.conversationPrivate && !this.canClientAccessChat()) { - this.events.fire_async("notify_conversation_state", { - chatId: this.chatId, - state: "private", - crossChannelChatSupported: this.crossChannelChatSupported - }); - return; - } - - this.events.fire_async("notify_conversation_state", { - chatId: this.chatId, - state: "normal", - - historyState: historyState, - historyErrorMessage: this.historyErrorMessage, - historyRetryTimestamp: this.historyRetryTimestamp, - - chatFrameMaxMessageCount: kMaxChatFrameMessageSize, - unreadTimestamp: this.unreadTimestamp, - - showUserSwitchEvents: this.conversationPrivate || !this.crossChannelChatSupported, - sendEnabled: this.messageSendEnabled, - - events: [...this.presentEvents, ...this.presentMessages] - }); - break; - - case "loading": - case "unloaded": - this.events.fire_async("notify_conversation_state", { - chatId: this.chatId, - state: "loading" - }); - break; - - case "error": - this.events.fire_async("notify_conversation_state", { - chatId: this.chatId, - state: "error", - errorMessage: this.errorMessage - }); - break; - - case "no-permissions": - this.events.fire_async("notify_conversation_state", { - chatId: this.chatId, - state: "no-permissions", - failedPermission: this.failedPermission - }); - break; - - } - } - - protected doSendMessage(message: string, targetMode: number, target: number) : Promise { - let msg = preprocessChatMessageForSend(message); - return this.connection.serverConnection.send_command("sendtextmessage", { - targetmode: targetMode, - cid: target, - target: target, - msg: msg - }, { process_result: false }).then(async () => true).catch(error => { - if(error instanceof CommandResult) { - if(error.id === ErrorID.PERMISSION_ERROR) { - this.registerChatEvent({ - type: "message-failed", - uniqueId: "msf-" + this.chatId + "-" + Date.now(), - timestamp: Date.now(), - error: "permission", - failedPermission: this.connection.permissions.resolveInfo(parseInt(error.json["failed_permid"]))?.name || tr("unknown") - }, false); - } else { - this.registerChatEvent({ - type: "message-failed", - uniqueId: "msf-" + this.chatId + "-" + Date.now(), - timestamp: Date.now(), - error: "error", - errorMessage: error.formattedMessage() - }, false); - } - } else if(typeof error === "string") { - this.registerChatEvent({ - type: "message-failed", - uniqueId: "msf-" + this.chatId + "-" + Date.now(), - timestamp: Date.now(), - error: "error", - errorMessage: error - }, false); - } else { - log.warn(LogCategory.CHAT, tr("Failed to send channel chat message to %s: %o"), this.chatId, error); - this.registerChatEvent({ - type: "message-failed", - uniqueId: "msf-" + this.chatId + "-" + Date.now(), - timestamp: Date.now(), - error: "error", - errorMessage: tr("lookup the console") - }, false); - } - return false; - }); - } - - public isUnread() { - return this.unreadTimestamp !== undefined; - } - - public setUnreadTimestamp(timestamp: number | undefined) { - if(timestamp === undefined) - this.lastReadMessage = Date.now(); - - if(this.unreadTimestamp === timestamp) - return; - - this.unreadTimestamp = timestamp; - this.events.fire_async("notify_unread_timestamp_changed", { chatId: this.chatId, timestamp: timestamp }); - } - - public jumpToPresent() { - this.reportStateToUI(); - } - - public uiQueryHistory(timestamp: number, enforce?: boolean) { - if(this.executingUIHistoryQuery && !enforce) - return; - - this.executingUIHistoryQuery = true; - this.queryHistory({ end: 1, begin: timestamp, limit: kMaxChatFrameMessageSize }).then(result => { - this.executingUIHistoryQuery = false; - this.historyErrorMessage = undefined; - this.historyRetryTimestamp = result.nextAllowedQuery; - - switch (result.status) { - case "success": - this.events.fire_async("notify_conversation_history", { - chatId: this.chatId, - state: "success", - - hasMoreMessages: result.moreEvents, - retryTimestamp: this.historyRetryTimestamp, - - events: result.events - }); - break; - - case "private": - this.events.fire_async("notify_conversation_history", { - chatId: this.chatId, - state: "error", - errorMessage: this.historyErrorMessage = tr("chat is private"), - retryTimestamp: this.historyRetryTimestamp - }); - break; - - case "no-permission": - this.events.fire_async("notify_conversation_history", { - chatId: this.chatId, - state: "error", - errorMessage: this.historyErrorMessage = tra("failed on {}", result.failedPermission || tr("unknown permission")), - retryTimestamp: this.historyRetryTimestamp - }); - break; - - case "error": - this.events.fire_async("notify_conversation_history", { - chatId: this.chatId, - state: "error", - errorMessage: this.historyErrorMessage = result.errorMessage, - retryTimestamp: this.historyRetryTimestamp - }); - break; - } - }); - } - - protected lastEvent() : ChatEvent | undefined { - if(this.presentMessages.length === 0) - return this.presentEvents.last(); - else if(this.presentEvents.length === 0 || this.presentMessages.last().timestamp > this.presentEvents.last().timestamp) - return this.presentMessages.last(); - else - return this.presentEvents.last(); - } - - protected sendMessageSendingEnabled(enabled: boolean) { - if(this.messageSendEnabled === enabled) - return; - - this.messageSendEnabled = enabled; - this.events.fire("notify_send_enabled", { chatId: this.chatId, enabled: enabled }); - } - - protected abstract canClientAccessChat() : boolean; - public abstract queryHistory(criteria: { begin?: number, end?: number, limit?: number }) : Promise; - public abstract queryCurrentMessages(); - public abstract sendMessage(text: string); -} - -export abstract class AbstractChatManager { - protected readonly uiEvents: Registry; - - protected constructor() { - this.uiEvents = new Registry(); - } - - handlePanelShow() { - this.uiEvents.fire("notify_panel_show"); - } - - protected abstract findChat(id: string) : AbstractChat; - - @EventHandler("query_conversation_state") - protected handleQueryConversationState(event: ConversationUIEvents["query_conversation_state"]) { - const conversation = this.findChat(event.chatId); - if(!conversation) { - this.uiEvents.fire_async("notify_conversation_state", { - state: "error", - errorMessage: tr("Unknown conversation"), - - chatId: event.chatId - }); - return; - } - - if(conversation.currentMode() === "unloaded") - conversation.queryCurrentMessages(); - else - conversation.reportStateToUI(); - } - - @EventHandler("query_conversation_history") - protected handleQueryHistory(event: ConversationUIEvents["query_conversation_history"]) { - const conversation = this.findChat(event.chatId); - if(!conversation) { - this.uiEvents.fire_async("notify_conversation_history", { - state: "error", - errorMessage: tr("Unknown conversation"), - retryTimestamp: Date.now() + 10 * 1000, - - chatId: event.chatId - }); - - log.error(LogCategory.CLIENT, tr("Tried to query history for an unknown conversation with id %s"), event.chatId); - return; - } - - conversation.uiQueryHistory(event.timestamp); - } - - @EventHandler("action_clear_unread_flag") - protected handleClearUnreadFlag(event: ConversationUIEvents["action_clear_unread_flag"]) { - this.findChat(event.chatId)?.setUnreadTimestamp(undefined); - } - - @EventHandler("action_self_typing") - protected handleActionSelfTyping(event: ConversationUIEvents["action_self_typing"]) { - if(this.findChat(event.chatId)?.isUnread()) - this.uiEvents.fire("action_clear_unread_flag", { chatId: event.chatId }); - } - - @EventHandler("action_send_message") - protected handleSendMessage(event: ConversationUIEvents["action_send_message"]) { - const conversation = this.findChat(event.chatId); - if(!conversation) { - log.error(LogCategory.CLIENT, tr("Tried to send a chat message to an unknown conversation with id %s"), event.chatId); - return; - } - - conversation.sendMessage(event.text); - } - - @EventHandler("action_jump_to_present") - protected handleJumpToPresent(event: ConversationUIEvents["action_jump_to_present"]) { - const conversation = this.findChat(event.chatId); - if(!conversation) { - log.error(LogCategory.CLIENT, tr("Tried to jump to present for an unknown conversation with id %s"), event.chatId); - return; - } - - conversation.jumpToPresent(); - } -} +import {AbstractChat, AbstractChatManager, kMaxChatFrameMessageSize} from "./AbstractConversion"; const kSuccessQueryThrottle = 5 * 1000; const kErrorQueryThrottle = 30 * 1000; diff --git a/shared/js/ui/frames/side/PrivateConversationManager.ts b/shared/js/ui/frames/side/PrivateConversationManager.ts index 9b89ed38..7027509f 100644 --- a/shared/js/ui/frames/side/PrivateConversationManager.ts +++ b/shared/js/ui/frames/side/PrivateConversationManager.ts @@ -14,10 +14,10 @@ import { ConversationHistoryResponse, ConversationUIEvents } from "tc-shared/ui/frames/side/ConversationDefinitions"; -import {AbstractChat, AbstractChatManager} from "tc-shared/ui/frames/side/ConversationManager"; import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; import {queryConversationEvents, registerConversationEvent} from "tc-shared/ui/frames/side/PrivateConversationHistory"; +import {AbstractChat, AbstractChatManager} from "tc-shared/ui/frames/side/AbstractConversion"; export type OutOfViewClient = { nickname: string, diff --git a/web/app/UnloadHandler.ts b/web/app/UnloadHandler.ts new file mode 100644 index 00000000..b0a25f12 --- /dev/null +++ b/web/app/UnloadHandler.ts @@ -0,0 +1,12 @@ +import {Settings, settings} from "tc-shared/settings"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; + +window.addEventListener("beforeunload", event => { + if(settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG)) + return; + + const active_connections = server_connections.all_connections().filter(e => e.connected); + if(active_connections.length == 0) return; + + event.returnValue = "Are you really sure?
You're still connected!"; +}); \ No newline at end of file diff --git a/web/app/index.ts b/web/app/index.ts index 4fbad1bb..71da3700 100644 --- a/web/app/index.ts +++ b/web/app/index.ts @@ -6,4 +6,6 @@ import "./hooks/ServerConnection"; import "./hooks/ExternalModal"; import "./hooks/AudioRecorder"; +import "./UnloadHandler"; + export = require("tc-shared/main"); \ No newline at end of file