Some minimal client improvements

canary
WolverinDEV 2020-08-22 17:50:38 +02:00
parent 1b8130e1fb
commit aa7b9fff43
12 changed files with 601 additions and 494 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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?<br>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();
}
/*

View File

@ -254,4 +254,9 @@ export function set_default_profile(profile: ConnectionProfile) {
export function delete_profile(profile: ConnectionProfile) {
available_profiles.remove(profile);
}
}
window.addEventListener("beforeunload", event => {
if(requires_save())
save();
});

View File

@ -795,15 +795,15 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
}
/* 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();

View File

@ -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<Events extends ConversationUIEvents> {
protected readonly connection: ConnectionHandler;
protected readonly chatId: string;
protected readonly events: Registry<Events>;
protected presentMessages: ChatEvent[] = [];
protected presentEvents: Exclude<ChatEvent, ChatEventMessage>[] = []; /* 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<Events>) {
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<boolean> {
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<ConversationHistoryResponse>;
public abstract queryCurrentMessages();
public abstract sendMessage(text: string);
}
export abstract class AbstractChatManager<Events extends ConversationUIEvents> {
protected readonly uiEvents: Registry<Events>;
protected constructor() {
this.uiEvents = new Registry<Events>();
}
handlePanelShow() {
this.uiEvents.fire("notify_panel_show");
}
protected abstract findChat(id: string) : AbstractChat<Events>;
@EventHandler<ConversationUIEvents>("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<ConversationUIEvents>("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<ConversationUIEvents>("action_clear_unread_flag")
protected handleClearUnreadFlag(event: ConversationUIEvents["action_clear_unread_flag"]) {
this.findChat(event.chatId)?.setUnreadTimestamp(undefined);
}
@EventHandler<ConversationUIEvents>("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<ConversationUIEvents>("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<ConversationUIEvents>("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();
}
}

View File

@ -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<Events extends ConversationUIEvents> {
protected readonly connection: ConnectionHandler;
protected readonly chatId: string;
protected readonly events: Registry<Events>;
protected presentMessages: ChatEvent[] = [];
protected presentEvents: Exclude<ChatEvent, ChatEventMessage>[] = []; /* 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<Events>) {
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<boolean> {
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<ConversationHistoryResponse>;
public abstract queryCurrentMessages();
public abstract sendMessage(text: string);
}
export abstract class AbstractChatManager<Events extends ConversationUIEvents> {
protected readonly uiEvents: Registry<Events>;
protected constructor() {
this.uiEvents = new Registry<Events>();
}
handlePanelShow() {
this.uiEvents.fire("notify_panel_show");
}
protected abstract findChat(id: string) : AbstractChat<Events>;
@EventHandler<ConversationUIEvents>("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<ConversationUIEvents>("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<ConversationUIEvents>("action_clear_unread_flag")
protected handleClearUnreadFlag(event: ConversationUIEvents["action_clear_unread_flag"]) {
this.findChat(event.chatId)?.setUnreadTimestamp(undefined);
}
@EventHandler<ConversationUIEvents>("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<ConversationUIEvents>("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<ConversationUIEvents>("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;

View File

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

12
web/app/UnloadHandler.ts Normal file
View File

@ -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?<br>You're still connected!";
});

View File

@ -6,4 +6,6 @@ import "./hooks/ServerConnection";
import "./hooks/ExternalModal";
import "./hooks/AudioRecorder";
import "./UnloadHandler";
export = require("tc-shared/main");