Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Added chat tab #14

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {ServerAddressInput} from "./ServerAddressInput";
import {TabModel} from "./tabs/TabModel";
import {WaitOverlay} from "./WaitOverlay";

import {ChatTabModel} from "./tabs/chat/ChatTabModel";
import {ChatTabView} from "./tabs/chat/ChatTabView";
import {ConsoleTabModel} from "./tabs/console/ConsoleTabModel";
import {ConsoleTabView} from "./tabs/console/ConsoleTabView";
import {GamesTabModel} from "./tabs/games/GamesTabModel";
Expand Down Expand Up @@ -46,6 +48,7 @@ class App extends RX.Component<{}, AppState> {
private tabs: Array<TabModel<any>> = [
new HomeTabModel(),
new ConsoleTabModel(),
new ChatTabModel(),
new GamesTabModel(),
new ModulesTabModel(),
new SettingsTabModel(),
Expand All @@ -56,12 +59,13 @@ class App extends RX.Component<{}, AppState> {
private tabViews = [
<HomeTabView model={this.tabs[0]} />,
<ConsoleTabView model={this.tabs[1]} />,
<GamesTabView model={this.tabs[2]} />,
<ModulesTabView model={this.tabs[3]} />,
<SettingsTabView model={this.tabs[4]} />,
<ServerAdminsTabView model={this.tabs[5]} />,
<UserManagementTabView model={this.tabs[6]} />,
<WorldMapTabView model={this.tabs[7]} />,
<ChatTabView model={this.tabs[2]} />,
<GamesTabView model={this.tabs[3]} />,
<ModulesTabView model={this.tabs[4]} />,
<SettingsTabView model={this.tabs[5]} />,
<ServerAdminsTabView model={this.tabs[6]} />,
<UserManagementTabView model={this.tabs[7]} />,
<WorldMapTabView model={this.tabs[8]} />,
];

constructor(props: {}) {
Expand Down
29 changes: 29 additions & 0 deletions src/tabs/chat/ChatTabController.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { TabController } from "../TabController";
import { ChatTabState, Message } from "./ChatTabState";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Message is an unused import.


export class ChatTabController extends TabController<ChatTabState> {
public static Commands = ["say", "whisper"];

public execute = () => {
const command: string = this.model.getState().commandToSend;
const m = this.model.getState();
const recipient: string = m.selectedRecipient;
if (!recipient || recipient === "ALL") {
this.model.requestResource({
data: "say " + command,
method: "POST",
resourcePath: ["console"],
});
} else {
const recipientName = recipient.replace("PLAYER_", "");
this.model.requestResource({
data: `whisper ${recipientName} ${command}`,
method: "POST",
resourcePath: ["console"],
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed in testing that whispering to a player doesn't give any feedback to the sender if on the frontend. I think it would be helpful, and it can be done by updating the state of the messages:
this.model.update({messages: this.model.getState().messages.concat({type: "CHAT", message: "message"})});
The in-game console uses the format "You -> {playername}: {message}".

}
this.model.update({
commandToSend: "",
});
}
}
65 changes: 65 additions & 0 deletions src/tabs/chat/ChatTabModel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ActionResult } from "../../io/ActionResult";
import { IncomingMessage } from "../../io/IncomingMessage";
import { OnlinePlayerMetadata } from "../../io/OnlinePlayerMetadata";
import { ResourcePath } from "../../io/ResourcePath";
import { ResourcePathUtil } from "../../io/ResourcePath";
import { ResourceSubscriberTabModel } from "../ResourceSubscriberTabModel";
import { TabController } from "../TabController";
import { ChatTabController } from "./ChatTabController";
import { ChatTabState, Message } from "./ChatTabState";

export class ChatTabModel extends ResourceSubscriberTabModel<ChatTabState> {

public getName(): string {
return "Chat";
}

public getSubscribedResourcePaths(): ResourcePath[] {
return [
["onlinePlayers"],
];
}

public getDefaultState(): ChatTabState {
return { messages: [], commandToSend: "Type a chat command here...", commands: [] };
}

public initController(): TabController<ChatTabState> {
return new ChatTabController();
}

public onResourceUpdated(resourcePath: ResourcePath, data: any): void {
if (ResourcePathUtil.equals(resourcePath, ["onlinePlayers"])) {
this.update({ onlinePlayers: data as OnlinePlayerMetadata[] });
}
}

public onMessage(message: IncomingMessage) {
super.onMessage(message);
if (message.messageType === "ACTION_RESULT") {
const innerMessage: ActionResult = message.data as ActionResult;
if (innerMessage.status !== "OK") {
// TODO: Maybe show error message here
}
}
if (message.messageType === "RESOURCE_EVENT" && ResourcePathUtil.equals(message.resourcePath, ["console"])) {
const oldState: ChatTabState = this.getState();
const msg: Message = (message.data) as Message;
if (msg.type === "CHAT" || msg.type === "CLIENT") {
this.update({ messages: oldState.messages.concat(msg) });
}
if (msg.type === "CONSOLE") {
if (msg.message === "Message sent") {
this.update({
messageSendStatus: "SENT",
});
} else if (msg.message === "User with name '[a-zA-Z0-9]+' not found.") {
this.update({
errorMessage: msg.message,
messageSendStatus: "ERROR",
});
}
}
}
}
}
16 changes: 16 additions & 0 deletions src/tabs/chat/ChatTabState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { OnlinePlayerMetadata } from "../../io/OnlinePlayerMetadata";

export interface Message {
type: "CONSOLE" | "CHAT" | "ERROR" | "NOTIFICATION" | "CLIENT";
message: string;
}

export interface ChatTabState {
messages?: Message[];
commandToSend?: string;
commands?: string[];
messageSendStatus?: "SENDING" | "SENT" | "ERROR" | "NONE";
errorMessage?: string;
selectedRecipient?: string;
onlinePlayers?: OnlinePlayerMetadata[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember that these are like global variables. Try to minimize the amount of variables in the state if you can.

}
128 changes: 128 additions & 0 deletions src/tabs/chat/ChatTabView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* tslint:disable:prefer-for-of */
/* tslint:disable:no-bitwise */

import RX = require("reactxp");
import Styles = require("../../styles/main");
import { TabView } from "../TabView";
import { ConsoleAutocomplete } from "./../console/ConsoleAutocomplete";
import { ChatTabController } from "./ChatTabController";
import { ChatTabState, Message } from "./ChatTabState";

interface MessagePart {
text: string;
color: number;
}

export class ChatTabView extends TabView<ChatTabState> {

private firstColor: number = 0xE000;
private lastColor: number = 0xEFFF;
private resetColor: number = 0xF000;
private colorStyles: any = {};

public render() {
const controller: ChatTabController = this.props.model.getController() as ChatTabController;
return (
<RX.ScrollView>
<RX.View style={[Styles.flex.column, Styles.flex.fill, Styles.justifyFlexEnd]}>
<RX.View>
{this.renderMessages()}
</RX.View>
</RX.View>
<RX.Text>
{this.renderStatusText()}</RX.Text>
<RX.View style={Styles.flex.row}>
<RX.TextInput
style={[Styles.whiteBox, Styles.commandTextInput]}
value={this.state.commandToSend}
onChangeText={this.onChangeValue}
autoFocus={true}
onKeyPress={(event) => { this.handleKeypress(event, controller); }} />
{this.renderRecipientList()}
<RX.Button style={Styles.okButton} onPress={controller.execute}><RX.Text>Send</RX.Text></RX.Button>
</RX.View>
</RX.ScrollView>
);
}

private onChangeValue = (newValue: string) => {
this.props.model.update({ commandToSend: newValue });
ConsoleAutocomplete.clear();
}

private renderRecipientList() {
const allItem = { label: "all", value: "ALL" };
return (
<RX.Picker
items={[allItem].concat(this.state.onlinePlayers.map((player) => ({ label: player.name, value: "PLAYER_" + player.name })))}
selectedValue={this.state.selectedRecipient}
onValueChange={(value, position) => { this.props.model.update({ selectedRecipient: value }); }} />
);
}

private renderMessages() {
return this.state.messages.map((msg: Message, index: number) =>
<RX.Text selectable={true} key={index}>{this.renderMessage(msg.message)}</RX.Text>);
}

private renderStatusText() {
switch (this.state.messageSendStatus) {
case "SENDING":
return "Sending...";
case "ERROR":
return this.state.errorMessage;
case "SENT":
return "Sent";
}
return "";
}

private renderMessage(message: string): JSX.Element {
const parts: MessagePart[] = [];
let currentPart: MessagePart = { text: "", color: 0 };
for (let i: number = 0; i < message.length; i++) {
const charCode: number = message.charCodeAt(i);
if (charCode === this.resetColor) {
parts.push(currentPart);
currentPart = { text: "", color: 0 };
} else if (charCode >= this.firstColor && charCode <= this.lastColor) {
if (currentPart.text !== "") {
parts.push(currentPart);
}
currentPart = { text: "", color: charCode };
} else {
currentPart.text += message[i];
}
}
parts.push(currentPart);
const renderedParts = parts.map((part: MessagePart, index: number) => {
const colorId: string = part.color.toString();
let colorStyle: RX.Types.TextStyle;
if (this.colorStyles.hasOwnProperty(colorId)) {
colorStyle = this.colorStyles[colorId];
} else {
colorStyle = RX.Styles.createTextStyle({ color: this.colorIdToRgbString(part.color) }) as RX.Types.TextStyle;
this.colorStyles[colorId] = colorStyle;
}
return <RX.Text key={index} style={colorStyle}>{part.text}</RX.Text>;
});
return (
<RX.Text>{renderedParts}</RX.Text>
);
}

private colorIdToRgbString(colorId: number): string {
const rgb: number = colorId - this.firstColor;
const r: number = ((rgb >> 8) & 0xF) << 4;
const g: number = ((rgb >> 4) & 0xF) << 4;
const b: number = ((rgb >> 0) & 0xF) << 4;
return "rgb(" + r.toString() + "," + g.toString() + "," + b.toString() + ")";
}

private handleKeypress(event: any, controller: ChatTabController) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be static.

if (event.keyCode === 13) {
controller.execute();
}
}

}