Improved channel private conversation mode behaviour

master
WolverinDEV 2020-12-09 14:22:22 +01:00 committed by WolverinDEV
parent fea0993c7d
commit 5407fcb9ff
9 changed files with 199 additions and 30 deletions

View File

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

View File

@ -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<Events extends AbstractChatEvents> {
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<Events extends AbstractChatEvents> {
this.connection = connection;
this.chatId = chatId;
this.unreadTimestamp = Date.now();
this.conversationMode = ChannelConversationMode.Public;
}
destroy() {
@ -103,7 +113,7 @@ export abstract class AbstractChat<Events extends AbstractChatEvents> {
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<Events extends AbstractChatEvents> {
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<Events extends AbstractChatEvents> {
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<Events extends AbstractChatEvents> {
this.events.fire("notify_send_toggle", { enabled: enabled });
}
public abstract canClientAccessChat() : boolean;
public abstract queryHistory(criteria: { begin?: number, end?: number, limit?: number }) : Promise<ConversationHistoryResponse>;
public abstract queryCurrentMessages();
public abstract sendMessage(text: string);

View File

@ -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<ChannelConversationEvents>
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<ChannelConversationEvents>
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<ChannelConversationEvents>
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<ChannelConversationEvents>
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<ChannelConversationEvents>
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<ChannelConversation> { }
@ -337,6 +349,37 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
}
}));
this.listenerConnection.push(connection.channelTree.events.on("notify_channel_updated", event => {
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)));

View File

@ -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<ChannelEvents> {
}
/* devel-block-end */
/* TODO: Validate values. Example: channel_conversation_mode */
for(let variable of variables) {
let key = variable.key;
let value = variable.value;

View File

@ -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<Events>();
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<any>) {
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<ConversationUIEvents>("query_conversation_state")
protected handleQueryConversationState(event: ConversationUIEvents["query_conversation_state"]) {
const conversation = this.conversationManager.findConversationById(event.chatId);

View File

@ -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<HTMLDivElement> }) => {
switch (props.event.newMode) {
case "none":
return (
<div className={cssStyle.containerSwitch + " " + cssStyle.actionClose} ref={props.refHTMLElement}>
<a><Translatable>The conversation has been disabled</Translatable></a>
<div />
</div>
);
case "private":
return (
<div className={cssStyle.containerSwitch + " " + cssStyle.actionClose} ref={props.refHTMLElement}>
<a><Translatable>The conversation has been made private</Translatable></a>
<div />
</div>
);
case "normal":
return (
<div className={cssStyle.containerSwitch + " " + cssStyle.actionClose} ref={props.refHTMLElement}>
<a><Translatable>The conversation has been made public</Translatable></a>
<div />
</div>
);
}
}
const PartnerTypingIndicator = (props: { events: Registry<ConversationUIEvents>, chatId: string, timeout?: number }) => {
const kTypingTimeout = props.timeout || 5000;
@ -658,6 +686,14 @@ class ConversationMessages extends React.PureComponent<ConversationMessagesPrope
refHTMLElement={reference}
/>);
break;
case "mode-changed":
this.viewEntries.push(<ChatEventModeChangedRenderer
key={event.uniqueId}
event={event}
refHTMLElement={reference}
/>);
break;
}
}
}

View File

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

View File

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

View File

@ -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 = <div className={cssStyle.value + " " + cssStyle.ping + " " + pingClass} key={"ping"} title={title}>{pingInfo.native.toFixed(0)}ms</div>;
}