Some minimal client improvements
parent
1b8130e1fb
commit
aa7b9fff43
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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!";
|
||||
});
|
|
@ -6,4 +6,6 @@ import "./hooks/ServerConnection";
|
|||
import "./hooks/ExternalModal";
|
||||
import "./hooks/AudioRecorder";
|
||||
|
||||
import "./UnloadHandler";
|
||||
|
||||
export = require("tc-shared/main");
|
Loading…
Reference in New Issue