Skip to content
Snippets Groups Projects
Commit 8520942c authored by danielm's avatar danielm
Browse files

Do all the things, for real

parent a75c5dd6
No related branches found
No related tags found
No related merge requests found
Pipeline #3687 failed
import React from 'react'
import { ConversationDto } from './chat.d'
import {proxy} from './Proxy'
export class ConversationCard extends React.Component<{
conversation: ConversationDto,
selected: boolean,
onSelect: () => void
}>
{
componentDidMount() {
proxy.addEventListener("message", (cid, m) => {
if (cid === this.props.conversation.channelId)
this.forceUpdate();
}, this);
}
componentWillUnmount() {
proxy.removeAllEventListener(this);
}
render() {
let lastMessage = this.props.conversation.lastMessages.length > 0 ?
this.props.conversation.lastMessages[this.props.conversation.lastMessages.length - 1] : null;
return (
<div className={"conversation-card" + (this.props.selected ? " selected" : "")}
onClick={() => this.props.onSelect()}>
<div className="row">
<span className="channel-name">{this.props.conversation.name}</span>
<span className="time">
{lastMessage && new Date(lastMessage.timeStamp).toLocaleDateString()}
</span>
</div>
<span className="last-message">{lastMessage?.content}</span>
</div>
);
}
}
export type EventMap = Record<string, Function>;
export class EventProducer<M extends EventMap> {
private listeners: {type: keyof M, listener: M[keyof M], obj?: Object}[] = [];
addEventListener<K extends keyof M>(type: K, listener: M[K], obj?: Object ) {
this.listeners.push({type, listener, obj});
}
removeEventListener<K extends keyof M>(type: K, listener: M[K]) {
this.listeners.splice(this.listeners.findIndex(x => x.type === type && x.listener === listener), 1);
}
removeAllEventListener(obj: Object) {
if (!obj)
throw new Error("Must specify object");
this.listeners = this.listeners.filter(x => x.obj !== obj);
}
protected dispatch<K extends keyof M>(type: K, ...args) {
for (let listener of this.listeners.filter(x => x.type === type))
listener.listener.call(listener.obj, ...args);
}
}
import React from 'react'
import {TextInputAndButton} from './TextInputAndButton'
import {ConversationCard} from './ConversationCard'
import { InboxDto, ConversationDto } from './chat.d'
import {proxy} from './Proxy'
export class LeftPane extends React.Component<{
inbox: InboxDto, selectedConversation: ConversationDto | undefined,
onSelect: (c: ConversationDto) => void
}>
{
sendContactRequest(email: string) {
proxy.sendPacket({type: "contactRequest", email, firstMessage: "Hello"});
return true;
}
componentDidMount() {
proxy.addEventListener("conversation", c => this.forceUpdate(), this);
}
componentWillUnmount() {
proxy.removeAllEventListener(this);
}
render() {
return (
<div className="left-pane">
<p className="my-tag">My tag: {this.props.inbox.user.tag}</p>
<TextInputAndButton type="text" placeholder="Add user by Tag (Name#123)"
buttonContent="Inv"
onClick={text => this.sendContactRequest(text)} />
<div className="conversations">
{this.props.inbox.conversations.map(x =>
<ConversationCard
key={x.channelId}
conversation={x}
selected={x === this.props.selectedConversation}
onSelect={() => this.props.onSelect(x)} />)}
</div>
</div>
);
}
}
import React from 'react';
import { proxy } from "./Proxy"
import { TextInput } from "./TextInput"
type LoginState = {
email: string,
password: string,
displayName: string,
register: boolean
};
export class Login extends React.Component<{}, LoginState> {
constructor(props) {
super(props);
this.state = { email: "", password: "", displayName: "", register: false };
}
onClick() {
if (this.state.register)
proxy.sendPacket({
type: "register", email: this.state.email, password: this.state.password,
displayName: this.state.displayName, staySignedIn: false
});
else
proxy.sendPacket({
type: "login", email: this.state.email, password: this.state.password,
staySignedIn: false
});
}
onEmailChange(v: string) {
if (v.startsWith("A72CSN")) {
this.setState({
email: v,
displayName: "Dániel Móna"
});
} else {
this.setState({
email: v
});
}
}
render() {
return (
<div className="login">
<img src="logo512.png" width="256" alt="Logo" />
{this.state.register &&
<TextInput type="text" placeholder="Display Name (Agent Smith)" value={this.state.displayName} onChange={v => this.setState({displayName: v})} onEnter={() => this.onClick()} />}
<TextInput type="email" placeholder="Email (someone@example.com)" autofocus={true} value={this.state.email} onChange={v => this.onEmailChange(v)} onEnter={() => this.onClick()} />
<TextInput type="password" placeholder="Password" value={ this.state.password } onChange={ v => this.setState({password: v}) } onEnter={() => this.onClick()} />
<button type="button" onClick={() => this.onClick()}>
{this.state.register ? "Register" : "Login"}
</button>
<p>{this.state.register ? "Switch back to " : "Have no account yet? Go and "}
<a href="" onClick={e => {e.preventDefault(); this.setState((prevState) => ({register: !prevState.register}));}}>
{this.state.register ? "Login" : "Register"}
</a>
</p>
<a href="https://www.google.hu/search?q=privacy">Privacy Policy</a>
</div>);
}
}
import React from 'react'
import {LeftPane} from './LeftPane'
import {RightPane} from './RightPane'
import { ConversationDto } from './chat.d'
import { proxy } from './Proxy'
export class Main extends React.Component {
state = {selectedConversation: undefined as (ConversationDto | undefined)};
render() {
let className = "main row " + (this.state.selectedConversation ? "right" : "left");
return (
<div className={className}>
<LeftPane
inbox={proxy.inbox!}
selectedConversation={this.state.selectedConversation}
onSelect={c => this.setState({selectedConversation: c})} />
<RightPane conversation={this.state.selectedConversation}
onBack={() => this.setState({selectedConversation: undefined})} />
</div>
);
}
}
import React from 'react'
import {MessageDto} from './chat';
export class MessageCard extends React.PureComponent<{message: MessageDto, own: boolean}>
{
render() {
return (
<div className={"message-card" + (this.props.own ? " own" : "")}>
<div className="bubble">
<span className="text">{this.props.message.content}</span>
<span className="time">
{new Date(this.props.message.timeStamp).toLocaleTimeString()}
</span>
</div>
</div>
);
}
}
import { InboxDto, MessageDto, IncomingPacket, OutgoingPacket } from "./chat.d"
import { EventMap, EventProducer } from "./EventProducer"
interface ProxyEventMap extends EventMap {
"login": () => void;
"message": (channelId: string, message: MessageDto) => void;
"conversation": (channelId: string) => void;
}
class Proxy extends EventProducer<ProxyEventMap> {
private ws: WebSocket;
inbox: InboxDto | null = null;
constructor() {
super();
this.ws = new WebSocket("wss://raja.aut.bme.hu/chat/");
this.ws.addEventListener("open", () => {
// this.ws.send("Hello");
});
this.ws.addEventListener("message", e => {
let p = JSON.parse(e.data) as IncomingPacket;
switch (p.type) {
case "error":
alert(p.message);
break;
case "login":
this.inbox = p.inbox;
this.dispatch( "login" );
break;
case "message":
let cid = p.channelId;
this.inbox!.conversations.find(x => x.channelId === cid)?.lastMessages.push(p.message);
this.dispatch( "message", cid, p.message );
break;
case "conversationAdded":
this.inbox!.conversations.push(p.conversation);
this.dispatch( "conversation", p.conversation.channelId );
break;
}
});
}
sendPacket(packet: OutgoingPacket) {
this.ws.send(JSON.stringify(packet));
}
}
export var proxy = new Proxy();
import React from 'react'
import {ConversationDto} from './chat';
import {proxy} from './Proxy';
import {TextInputAndButton} from './TextInputAndButton';
import {MessageCard} from './MessageCard';
export class RightPane extends React.Component<{conversation?: ConversationDto, onBack: () => void }>
{
componentDidMount() {
proxy.addEventListener("message", (cid, m) => {
if (cid === this.props.conversation?.channelId)
this.forceUpdate();
}, this);
}
componentWillUnmount() {
proxy.removeAllEventListener(this);
}
onSend(text: string) {
proxy.sendPacket({
type: "message", channelId: this.props.conversation!.channelId, referenceTo: 0
, contentType: 0, content: "A: "+text
});
return true;
}
render() {
return (
<div className="right-pane column">
{this.props.conversation &&
<>
<div className="conversation-header row">
<button type="button" className="only-narrow"
onClick={() => this.props.onBack()}>Back</button>
<p>{this.props.conversation?.name}</p>
</div>
<div className="messages">
{this.props.conversation?.lastMessages.map(x =>
<MessageCard key={x.id} message={x}
own={x.senderId === proxy.inbox?.user.id} />)}
</div>
<div className="send-message row">
<TextInputAndButton type="text" placeholder="Type something awesome here..."
buttonContent="Send" onClick={x => this.onSend(x)} />
</div>
</>
}
</div>
);
}
}
.text-input {
position: relative;
padding: 12px 0;
}
.text-input input {
width: 100%;
padding: 6px 6px;
border: 0;
outline: none;
}
.text-input label {
position: absolute;
color: gray;
font-style: italic;
left: 6px;
top: 18px;
pointer-events: none;
transition: all 0.15s;
}
.text-input label.subsided {
left: 0;
top: 0;
font-size: 9pt;
transition: all 0.15s;
color: lightgray;
}
.text-input .focus-indicator {
position: absolute;
bottom: 12px;
width: 100%;
border: solid gray;
border-width: 0 0 1px 0;
}
.text-input input:focus + .focus-indicator {
border-color: black;
border-color: var( --focus-indicator-color, black);
border-width: 0 0 2px 0;
}
import React from 'react'
import './TextInput.css'
export interface TextInputProps {
value?: string;
onChange?: (value: string) => void;
type?: "text" | "password" | "email";
placeholder?: string;
onEnter?: () => void;
autofocus?: boolean;
};
type TextInputState = {
focus: boolean;
};
export class TextInput extends React.Component<TextInputProps, TextInputState> {
constructor(props: TextInputProps) {
super(props);
this.state = {focus: false};
}
render() {
let attrs = {} as any;
if (this.props.autofocus)
attrs.autoFocus = true;
if (this.props.onEnter)
attrs.onKeyDown = e => {
if (e.keyCode === 13)
this.props.onEnter!();
};
return (
<div className="text-input">
<input type={this.props.type ?? "text"} value={this.props.value}
onChange={e => {
this.props.onChange?.(e.target.value);
}}
onBlur={() => this.setState({focus: false})}
onFocus={() => this.setState({focus: true})}
{ ...attrs }
/>
<div className="focus-indicator"></div>
<label className={this.props.value || this.state.focus ? "subsided" : ""}>
{this.props.placeholder}
</label>
</div>);
}
}
import React from 'react'
import {TextInputProps, TextInput} from "./TextInput"
export interface TextInputAndButtonProps extends TextInputProps {
buttonContent?: string;
onClick?: (text: string) => boolean | void;
}
interface TextInputAndButtonState {
value: string;
}
export class TextInputAndButton extends React.Component<TextInputAndButtonProps, TextInputAndButtonState> {
constructor(props: TextInputAndButtonProps) {
super(props);
this.state = { value: "" };
}
onClick() {
if (this.props.onClick!(this.state.value))
this.setState({value: ""});
}
render() {
return (
<div className="text-input-and-button">
<TextInput value={this.state.value} {...this.props} onChange={v => this.setState({value: v})} onEnter={() => this.onClick()} />
<button type="button" onClick={() => this.onClick()}>
{this.props.buttonContent}
</button>
</div>
);
}
}
export interface MessageDto {
id: number;
timeStamp: string;
referenceTo: number; // 0: normal message, +: update, -: delete
senderId: string;
contentType: number;
content: string;
}
export interface UserDto {
id: string;
displayName: string;
tag: string;
lastSeen: string;
}
export interface ConversationDto {
channelId: string;
parentChannelId: string;
name: string;
description: string;
data: string;
state: number; // disconnected, outgoingRequest, incomingRequest, accepted, group
access: number; // none, read, write, admin
notificationLevel: number; // none, gray, push
unreadCount: number;
memberIds: string[];
lastMessages: MessageDto[];
}
export interface InboxDto {
user: UserDto;
contacts: UserDto[];
conversations: ConversationDto[];
}
export type OutgoingPacket =
{type: "login", email: string, password: string, staySignedIn: boolean} |
{type: "loginWithToken", token: string} |
{type: "register", email: string, password: string, displayName: string, staySignedIn: boolean} |
{type: "contactRequest", email: string, firstMessage: string} |
{type: "message", channelId: string, referenceTo: number, contentType: number, content: string};
export type IncomingPacket =
{type: "error", message: string} |
{type: "login", query: string, token: string, inbox: InboxDto} |
{type: "message", channelId: string, message: MessageDto} |
{type: "conversationAdded", conversation: ConversationDto} |
{type: "conversationRemoved", channelId: string} |
{type: "user", user: UserDto};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment