From 5407fcb9ffc72a8f9bc5ddf6a197759f23a356e6 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Wed, 9 Dec 2020 14:22:22 +0100 Subject: [PATCH] Improved channel private conversation mode behaviour --- ChangeLog.md | 1 + shared/js/conversations/AbstractConversion.ts | 58 ++++++++++++++++--- .../ChannelConversationManager.ts | 55 ++++++++++++++++-- shared/js/tree/Channel.ts | 10 +++- .../side/AbstractConversationController.ts | 26 +++++++-- .../side/AbstractConversationRenderer.tsx | 38 +++++++++++- .../side/ChannelConversationController.ts | 29 ++++++++-- .../ui/frames/side/ConversationDefinitions.ts | 8 ++- shared/js/ui/frames/side/HeaderRenderer.tsx | 4 +- 9 files changed, 199 insertions(+), 30 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 0a6b3256..c7ce7b2e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -2,6 +2,7 @@ * **09.12.20** - Fixed the private messages unread indicator - Properly updating the private message unread count + - Improved channel conversation mode detection support * **08.12.20** - Fixed the permission editor not resolving unique ids diff --git a/shared/js/conversations/AbstractConversion.ts b/shared/js/conversations/AbstractConversion.ts index db08886e..26cc04ec 100644 --- a/shared/js/conversations/AbstractConversion.ts +++ b/shared/js/conversations/AbstractConversion.ts @@ -2,15 +2,17 @@ import { ChatEvent, ChatEventMessage, ChatMessage, - ChatState, ConversationHistoryResponse + ChatState, + ConversationHistoryResponse } from "tc-shared/ui/frames/side/ConversationDefinitions"; import {Registry} from "tc-shared/events"; -import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {preprocessChatMessageForSend} from "tc-shared/text/chat"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {ErrorCode} from "tc-shared/connection/ErrorCode"; import {LogCategory, logWarn} from "tc-shared/log"; -import {ChannelConversation} from "tc-shared/conversations/ChannelConversationManager"; +import {ChannelConversationMode} from "tc-shared/tree/Channel"; +import {guid} from "tc-shared/crypto/uid"; export const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */ @@ -34,6 +36,12 @@ export interface AbstractChatEvents { }, notify_history_state_changed: { hasHistory: boolean + }, + notify_conversation_mode_changed: { + newMode: ChannelConversationMode + }, + notify_read_state_changed: { + readable: boolean } } @@ -50,13 +58,14 @@ export abstract class AbstractChat { protected failedPermission: string; protected errorMessage: string; - protected conversationPrivate: boolean = false; + private conversationMode: ChannelConversationMode; protected crossChannelChatSupported: boolean = true; protected unreadTimestamp: number; protected unreadState: boolean = false; protected messageSendEnabled: boolean = true; + private conversationReadable = true; private history = false; @@ -65,6 +74,7 @@ export abstract class AbstractChat { this.connection = connection; this.chatId = chatId; this.unreadTimestamp = Date.now(); + this.conversationMode = ChannelConversationMode.Public; } destroy() { @@ -103,7 +113,7 @@ export abstract class AbstractChat { this.setUnreadTimestamp(Date.now()); } else if(!this.isUnread() && triggerUnread) { this.setUnreadTimestamp(event.message.timestamp - 1); - } else { + } else if(!this.isUnread()) { /* mark the last message as read */ this.setUnreadTimestamp(event.message.timestamp); } @@ -119,7 +129,7 @@ export abstract class AbstractChat { if(!this.isUnread() && triggerUnread) { this.setUnreadTimestamp(event.timestamp - 1); - } else { + } else if(!this.isUnread()) { /* mark the last message as read */ this.setUnreadTimestamp(event.timestamp); } @@ -197,8 +207,41 @@ export abstract class AbstractChat { return this.unreadState; } + public getConversationMode() : ChannelConversationMode { + return this.conversationMode; + } + public isPrivate() : boolean { - return this.conversationPrivate; + return this.conversationMode === ChannelConversationMode.Private; + } + + protected setConversationMode(mode: ChannelConversationMode) { + if(this.conversationMode === mode) { + return; + } + + this.registerChatEvent({ + type: "mode-changed", + uniqueId: guid() + "-mode-change", + timestamp: Date.now(), + newMode: mode === ChannelConversationMode.Public ? "normal" : mode === ChannelConversationMode.Private ? "private" : "none" + }, true); + + this.conversationMode = mode; + this.events.fire("notify_conversation_mode_changed", { newMode: mode }); + } + + public isReadable() { + return this.conversationReadable; + } + + protected setReadable(flag: boolean) { + if(this.conversationReadable === flag) { + return; + } + + this.conversationReadable = flag; + this.events.fire("notify_read_state_changed", { readable: flag }); } public isSendEnabled() : boolean { @@ -275,7 +318,6 @@ export abstract class AbstractChat { this.events.fire("notify_send_toggle", { enabled: enabled }); } - public abstract canClientAccessChat() : boolean; public abstract queryHistory(criteria: { begin?: number, end?: number, limit?: number }) : Promise; public abstract queryCurrentMessages(); public abstract sendMessage(text: string); diff --git a/shared/js/conversations/ChannelConversationManager.ts b/shared/js/conversations/ChannelConversationManager.ts index 604272ef..0adb68a8 100644 --- a/shared/js/conversations/ChannelConversationManager.ts +++ b/shared/js/conversations/ChannelConversationManager.ts @@ -15,6 +15,7 @@ import {traj} from "tc-shared/i18n/localize"; import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; import {LocalClientEntry} from "tc-shared/tree/Client"; import {ServerCommand} from "tc-shared/connection/ConnectionBase"; +import {ChannelConversationMode} from "tc-shared/tree/Channel"; export interface ChannelConversationEvents extends AbstractChatEvents { notify_messages_deleted: { messages: string[] }, @@ -40,13 +41,12 @@ export class ChannelConversation extends AbstractChat this.conversationId = id; this.preventUnreadUpdate = true; - const dateNow = Date.now(); const unreadTimestamp = handle.connection.settings.server(Settings.FN_CHANNEL_CHAT_READ(id), Date.now()); this.setUnreadTimestamp(unreadTimestamp); this.preventUnreadUpdate = false; - this.events.on("notify_unread_state_changed", event => { - this.handle.connection.channelTree.findChannel(this.conversationId)?.setUnread(event.unread); + this.events.on(["notify_unread_state_changed", "notify_read_state_changed"], event => { + this.handle.connection.channelTree.findChannel(this.conversationId)?.setUnread(this.isReadable() && this.isUnread()); }); } @@ -142,7 +142,6 @@ export class ChannelConversation extends AbstractChat this.setCurrentMode("loading"); this.queryHistory({ end: 1, limit: kMaxChatFrameMessageSize }).then(history => { - this.conversationPrivate = false; this.conversationVolatile = false; this.failedPermission = undefined; this.errorMessage = undefined; @@ -166,17 +165,18 @@ export class ChannelConversation extends AbstractChat break; case "private": - this.conversationPrivate = true; + this.setConversationMode(ChannelConversationMode.Private); this.setCurrentMode("normal"); break; case "success": + this.setConversationMode(ChannelConversationMode.Public); this.setCurrentMode("normal"); break; case "unsupported": this.crossChannelChatSupported = false; - this.conversationPrivate = true; + this.setConversationMode(ChannelConversationMode.Private); this.setCurrentMode("normal"); break; } @@ -295,6 +295,10 @@ export class ChannelConversation extends AbstractChat this.handle.connection.settings.changeServer(Settings.FN_CHANNEL_CHAT_READ(this.conversationId), timestamp); } + public setConversationMode(mode: ChannelConversationMode) { + super.setConversationMode(mode); + } + public localClientSwitchedChannel(type: "join" | "leave") { this.registerChatEvent({ type: "local-user-switch", @@ -309,6 +313,14 @@ export class ChannelConversation extends AbstractChat sendMessage(text: string) { this.doSendMessage(text, this.conversationId ? 2 : 3, this.conversationId).then(() => {}); } + + updateAccessState() { + if(this.isPrivate()) { + this.setReadable(this.connection.getClient().currentChannel()?.getChannelId() === this.conversationId); + } else { + this.setReadable(true); + } + } } export interface ChannelConversationManagerEvents extends AbstractChatManagerEvents { } @@ -337,6 +349,37 @@ export class ChannelConversationManager extends AbstractChatManager { + const conversation = this.findConversation(event.channel.channelId); + if(!conversation) { + return; + } + + if("channel_conversation_mode" in event.updatedProperties) { + conversation.setConversationMode(event.channel.properties.channel_conversation_mode); + conversation.updateAccessState(); + } + })); + + this.listenerConnection.push(connection.channelTree.events.on("notify_client_moved", event => { + if(event.client instanceof LocalClientEntry) { + const fromConversation = this.findConversation(event.oldChannel.channelId); + const targetConversation = this.findConversation(event.newChannel.channelId); + + fromConversation?.updateAccessState(); + targetConversation?.updateAccessState(); + } + })); + + this.listenerConnection.push(connection.channelTree.events.on("notify_client_enter_view", event => { + if(event.client instanceof LocalClientEntry) { + const targetConversation = this.findConversation(event.targetChannel.channelId); + targetConversation?.updateAccessState(); + } + })); + + /* TODO: Permission listener for text send power! */ + this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationhistory", this.handleConversationHistory.bind(this))); this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationindex", this.handleConversationIndex.bind(this))); this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationmessagedelete", this.handleConversationMessageDelete.bind(this))); diff --git a/shared/js/tree/Channel.ts b/shared/js/tree/Channel.ts index fc73209a..26ec80c4 100644 --- a/shared/js/tree/Channel.ts +++ b/shared/js/tree/Channel.ts @@ -42,6 +42,12 @@ export enum ChannelSubscribeMode { INHERITED } +export enum ChannelConversationMode { + Public = 0, + Private = 1, + None = 2 +} + export class ChannelProperties { channel_order: number = 0; channel_name: string = ""; @@ -73,7 +79,7 @@ export class ChannelProperties { //Only after request channel_description: string = ""; - channel_conversation_mode: number = 0; /* 0 := Private, the default */ + channel_conversation_mode: ChannelConversationMode = 0; channel_conversation_history_length: number = -1; } @@ -564,6 +570,8 @@ export class ChannelEntry extends ChannelTreeEntry { } /* devel-block-end */ + /* TODO: Validate values. Example: channel_conversation_mode */ + for(let variable of variables) { let key = variable.key; let value = variable.value; diff --git a/shared/js/ui/frames/side/AbstractConversationController.ts b/shared/js/ui/frames/side/AbstractConversationController.ts index 5032befa..80999ca4 100644 --- a/shared/js/ui/frames/side/AbstractConversationController.ts +++ b/shared/js/ui/frames/side/AbstractConversationController.ts @@ -35,6 +35,8 @@ export abstract class AbstractConversationController< protected currentSelectedConversation: ConversationType; protected currentSelectedListener: (() => void)[]; + protected crossChannelChatSupported = true; + protected constructor(conversationManager: Manager) { this.uiEvents = new Registry(); this.currentSelectedListener = []; @@ -86,6 +88,10 @@ export abstract class AbstractConversationController< this.currentSelectedListener.push(conversation.events.on("notify_history_state_changed", () => { this.reportStateToUI(conversation); })); + + this.currentSelectedListener.push(conversation.events.on("notify_read_state_changed", () => { + this.reportStateToUI(conversation); + })); } handlePanelShow() { @@ -93,8 +99,6 @@ export abstract class AbstractConversationController< } protected reportStateToUI(conversation: AbstractChat) { - const crossChannelChatSupported = true; /* FIXME: Determine this form the server! */ - let historyState: ChatHistoryState; const localHistoryState = this.historyUiStates[conversation.getChatId()]; if(!localHistoryState) { @@ -113,11 +117,11 @@ export abstract class AbstractConversationController< switch (conversation.getCurrentMode()) { case "normal": - if(conversation.isPrivate() && !conversation.canClientAccessChat()) { + if(conversation.isPrivate() && !conversation.isReadable()) { this.uiEvents.fire_react("notify_conversation_state", { chatId: conversation.getChatId(), state: "private", - crossChannelChatSupported: crossChannelChatSupported + crossChannelChatSupported: this.crossChannelChatSupported }); return; } @@ -133,7 +137,7 @@ export abstract class AbstractConversationController< chatFrameMaxMessageCount: kMaxChatFrameMessageSize, unreadTimestamp: conversation.getUnreadTimestamp(), - showUserSwitchEvents: conversation.isPrivate() || !crossChannelChatSupported, + showUserSwitchEvents: conversation.isPrivate() || !this.crossChannelChatSupported, sendEnabled: conversation.isSendEnabled(), events: [...conversation.getPresentEvents(), ...conversation.getPresentMessages()] @@ -230,6 +234,18 @@ export abstract class AbstractConversationController< return this.currentSelectedConversation; } + protected setCrossChannelChatSupport(flag: boolean) { + if(this.crossChannelChatSupported === flag) { + return; + } + + this.crossChannelChatSupported = flag; + const currentConversation = this.getCurrentConversation(); + if(currentConversation) { + this.reportStateToUI(this.getCurrentConversation()); + } + } + @EventHandler("query_conversation_state") protected handleQueryConversationState(event: ConversationUIEvents["query_conversation_state"]) { const conversation = this.conversationManager.findConversationById(event.chatId); diff --git a/shared/js/ui/frames/side/AbstractConversationRenderer.tsx b/shared/js/ui/frames/side/AbstractConversationRenderer.tsx index 4c2f9f5d..e4bd599e 100644 --- a/shared/js/ui/frames/side/AbstractConversationRenderer.tsx +++ b/shared/js/ui/frames/side/AbstractConversationRenderer.tsx @@ -15,7 +15,7 @@ import { ChatEventPartnerAction, ChatHistoryState, ChatMessage, - ConversationUIEvents + ConversationUIEvents, ChatEventModeChanged } from "tc-shared/ui/frames/side/ConversationDefinitions"; import {TimestampRenderer} from "tc-shared/ui/react-elements/TimestampRenderer"; import {BBCodeRenderer} from "tc-shared/text/bbcode"; @@ -277,6 +277,34 @@ const ChatEventPartnerActionRenderer = (props: { event: ChatEventPartnerAction, return null; }; +const ChatEventModeChangedRenderer = (props: { event: ChatEventModeChanged, refHTMLElement: Ref }) => { + switch (props.event.newMode) { + case "none": + return ( +
+ The conversation has been disabled +
+
+ ); + + case "private": + return ( +
+ The conversation has been made private +
+
+ ); + + case "normal": + return ( +
+ The conversation has been made public +
+
+ ); + } +} + const PartnerTypingIndicator = (props: { events: Registry, chatId: string, timeout?: number }) => { const kTypingTimeout = props.timeout || 5000; @@ -658,6 +686,14 @@ class ConversationMessages extends React.PureComponent); break; + + case "mode-changed": + this.viewEntries.push(); + break; } } } diff --git a/shared/js/ui/frames/side/ChannelConversationController.ts b/shared/js/ui/frames/side/ChannelConversationController.ts index 10d02062..11d3fd5f 100644 --- a/shared/js/ui/frames/side/ChannelConversationController.ts +++ b/shared/js/ui/frames/side/ChannelConversationController.ts @@ -1,20 +1,20 @@ import * as React from "react"; -import {ConnectionHandler} from "../../../ConnectionHandler"; +import {ConnectionHandler, ConnectionState} from "../../../ConnectionHandler"; import {EventHandler} from "../../../events"; import * as log from "../../../log"; import {LogCategory} from "../../../log"; import {tr} from "../../../i18n/localize"; -import ReactDOM = require("react-dom"); -import { - ConversationUIEvents -} from "../../../ui/frames/side/ConversationDefinitions"; +import {ConversationUIEvents} from "../../../ui/frames/side/ConversationDefinitions"; import {ConversationPanel} from "./AbstractConversationRenderer"; import {AbstractConversationController} from "./AbstractConversationController"; import { - ChannelConversation, ChannelConversationEvents, + ChannelConversation, + ChannelConversationEvents, ChannelConversationManager, ChannelConversationManagerEvents } from "tc-shared/conversations/ChannelConversationManager"; +import {ServerFeature} from "tc-shared/connection/ServerFeatures"; +import ReactDOM = require("react-dom"); export class ChannelConversationController extends AbstractConversationController< ConversationUIEvents, @@ -61,6 +61,18 @@ export class ChannelConversationController extends AbstractConversationControlle })); this.uiEvents.register_handler(this, true); + + this.listenerManager.push(connection.events().on("notify_connection_state_changed", event => { + if(event.newState === ConnectionState.CONNECTED) { + connection.serverFeatures.awaitFeatures().then(success => { + if(!success) { return; } + + this.setCrossChannelChatSupport(connection.serverFeatures.supportsFeature(ServerFeature.ADVANCED_CHANNEL_CHAT)); + }); + } else { + this.setCrossChannelChatSupport(true); + } + })); } destroy() { @@ -84,8 +96,13 @@ export class ChannelConversationController extends AbstractConversationControlle protected registerConversationEvents(conversation: ChannelConversation) { super.registerConversationEvents(conversation); + this.currentSelectedListener.push(conversation.events.on("notify_messages_deleted", event => { this.uiEvents.fire_react("notify_chat_message_delete", { messageIds: event.messages, chatId: conversation.getChatId() }); })); + + this.currentSelectedListener.push(conversation.events.on("notify_conversation_mode_changed", () => { + this.reportStateToUI(conversation); + })); } } \ No newline at end of file diff --git a/shared/js/ui/frames/side/ConversationDefinitions.ts b/shared/js/ui/frames/side/ConversationDefinitions.ts index 253ff520..e7dd6a3e 100644 --- a/shared/js/ui/frames/side/ConversationDefinitions.ts +++ b/shared/js/ui/frames/side/ConversationDefinitions.ts @@ -16,7 +16,8 @@ export type ChatEvent = { timestamp: number; uniqueId: string; } & ( ChatEventQueryFailed | ChatEventPartnerInstanceChanged | ChatEventLocalAction | - ChatEventPartnerAction + ChatEventPartnerAction | + ChatEventModeChanged ); export interface ChatEventUnreadTrigger { @@ -64,6 +65,11 @@ export interface ChatEventPartnerAction { action: "disconnect" | "close" | "reconnect"; } +export interface ChatEventModeChanged { + type: "mode-changed"; + newMode: "normal" | "private" | "none" +} + /* ---------- Chat States ---------- */ export type ChatState = "normal" | "loading" | "no-permissions" | "error" | "unloaded"; export type ChatHistoryState = "none" | "loading" | "available" | "error"; diff --git a/shared/js/ui/frames/side/HeaderRenderer.tsx b/shared/js/ui/frames/side/HeaderRenderer.tsx index f3a160a5..645129f3 100644 --- a/shared/js/ui/frames/side/HeaderRenderer.tsx +++ b/shared/js/ui/frames/side/HeaderRenderer.tsx @@ -105,9 +105,9 @@ const BlockPing = () => { } if(pingInfo.javaScript === undefined) { - title = tr("Ping: " + pingInfo.native.toFixed(3) + "ms"); + title = tra("Ping: {}ms", pingInfo.native.toFixed(3)); } else { - title = tr("Native: " + pingInfo.native.toFixed(3) + "ms\nJavascript: " + pingInfo.javaScript.toFixed(3) + "ms"); + title = tra("Native: {}ms\nJavascript: {}ms", pingInfo.native.toFixed(3), pingInfo.javaScript.toFixed(3)); } value =
{pingInfo.native.toFixed(0)}ms
; }