Skip to content

Commit 0ad1d6d

Browse files
authored
Chat messages (#871)
* Add chat message functionality * Add chat to chess example. * Only set chat functions when transport supports it * Add id to chat messages using shortid * Add tests for chat functionality * Update with changes to auth system * Changed type from chat-message to just chat
1 parent 9b3e2a9 commit 0ad1d6d

File tree

16 files changed

+236
-2
lines changed

16 files changed

+236
-2
lines changed

examples/react-web/src/chess/board.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import PropTypes from 'prop-types';
1111
import { Chess } from 'chess.js';
1212
import { Checkerboard, cartesianToAlgebraic } from './checkerboard';
1313
import { Token } from './token';
14+
import Chat from './chat';
1415
import Bishop from './pieces/bishop';
1516
import King from './pieces/king';
1617
import Knight from './pieces/knight';
@@ -31,6 +32,8 @@ class Board extends React.Component {
3132
isActive: PropTypes.bool,
3233
isMultiplayer: PropTypes.bool,
3334
isConnected: PropTypes.bool,
35+
sendChatMessage: PropTypes.func,
36+
chatMessages: PropTypes.array,
3437
};
3538

3639
constructor(props) {
@@ -64,6 +67,12 @@ class Board extends React.Component {
6467

6568
{this._getStatus()}
6669
{disconnected}
70+
{this.props.sendChatMessage && this.props.chatMessages && (
71+
<Chat
72+
onSend={this.props.sendChatMessage}
73+
messages={this.props.chatMessages}
74+
/>
75+
)}
6776
</div>
6877
);
6978
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { useState } from 'react';
2+
3+
const Chat = ({ onSend, messages }) => {
4+
const [message, setMessage] = useState('');
5+
6+
const onChange = event => {
7+
setMessage(event.target.value);
8+
};
9+
10+
const triggerSend = () => {
11+
onSend(message);
12+
setMessage('');
13+
};
14+
15+
return (
16+
<div>
17+
<div
18+
style={{
19+
height: 200,
20+
maxWidth: 400,
21+
overflow: 'scroll',
22+
border: '1px solid black',
23+
}}
24+
>
25+
{messages.map(message => (
26+
<div key={message.id}>
27+
<div>{message.sender}</div>
28+
<div>{JSON.stringify(message.payload)}</div>
29+
</div>
30+
))}
31+
</div>
32+
<input onChange={onChange} value={message} />
33+
<button onClick={triggerSend}>Send</button>
34+
</div>
35+
);
36+
};
37+
38+
export default Chat;

src/client/client.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,21 @@ describe('multiplayer', () => {
164164
client.moves.A();
165165
expect(client.transport.onAction).toHaveBeenCalled();
166166
});
167+
168+
test('Sends and receives chat messages', () => {
169+
const fn = jest.fn();
170+
jest.spyOn(client.transport, 'onAction');
171+
client.updatePlayerID('0');
172+
client.updateMatchID('matchID');
173+
jest.spyOn(client.transport, 'onChatMessage');
174+
175+
client.sendChatMessage({ message: 'foo' });
176+
177+
expect(client.transport.onChatMessage).toHaveBeenCalledWith(
178+
'matchID',
179+
expect.objectContaining({ payload: { message: 'foo' }, sender: '0' })
180+
);
181+
});
167182
});
168183

169184
describe('multiplayer: SocketIO()', () => {
@@ -232,6 +247,25 @@ describe('multiplayer', () => {
232247

233248
expect(client0.getState().G).toEqual({ A: '1' });
234249
expect(client1.getState().G).toEqual({ A: '1' });
250+
251+
client0.sendChatMessage({ message: 'foo' });
252+
253+
expect(client0.chatMessages).toEqual([
254+
expect.objectContaining({ sender: '0', payload: { message: 'foo' } }),
255+
]);
256+
expect(client1.chatMessages).toEqual([
257+
expect.objectContaining({ sender: '0', payload: { message: 'foo' } }),
258+
]);
259+
260+
client1.sendChatMessage({ message: 'bar' });
261+
expect(client0.chatMessages).toEqual([
262+
expect.objectContaining({ sender: '0', payload: { message: 'foo' } }),
263+
expect.objectContaining({ sender: '1', payload: { message: 'bar' } }),
264+
]);
265+
expect(client1.chatMessages).toEqual([
266+
expect.objectContaining({ sender: '0', payload: { message: 'foo' } }),
267+
expect.objectContaining({ sender: '1', payload: { message: 'bar' } }),
268+
]);
235269
});
236270
});
237271

src/client/client.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* https://opensource.org/licenses/MIT.
77
*/
88

9+
import shortid from 'shortid';
910
import 'svelte';
1011
import {
1112
Dispatch,
@@ -35,6 +36,7 @@ import {
3536
State,
3637
Store,
3738
Ctx,
39+
ChatMessage,
3840
} from '../types';
3941

4042
type ClientAction = ActionShape.Reset | ActionShape.Sync | ActionShape.Update;
@@ -160,6 +162,8 @@ export class _ClientImpl<G extends any = any> {
160162
reset: () => void;
161163
undo: () => void;
162164
redo: () => void;
165+
sendChatMessage: (message: ChatMessage) => void;
166+
chatMessages: ChatMessage[];
163167

164168
constructor({
165169
game,
@@ -322,6 +326,7 @@ export class _ClientImpl<G extends any = any> {
322326
onAction: () => {},
323327
subscribe: () => {},
324328
subscribeMatchData: () => {},
329+
subscribeChatMessage: () => {},
325330
connect: () => {},
326331
disconnect: () => {},
327332
updateMatchID: () => {},
@@ -348,6 +353,21 @@ export class _ClientImpl<G extends any = any> {
348353
this.matchData = metadata;
349354
this.notifySubscribers();
350355
});
356+
357+
if (this.transport.onChatMessage) {
358+
this.chatMessages = [];
359+
this.sendChatMessage = payload => {
360+
this.transport.onChatMessage(this.matchID, {
361+
id: shortid(),
362+
sender: this.playerID,
363+
payload: payload,
364+
});
365+
};
366+
this.transport.subscribeChatMessage(message => {
367+
this.chatMessages = [...this.chatMessages, message];
368+
this.notifySubscribers();
369+
});
370+
}
351371
}
352372

353373
private notifySubscribers() {

src/client/react-native.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ export function Client(opts) {
112112
undo: this.client.undo,
113113
redo: this.client.redo,
114114
matchData: this.client.matchData,
115+
sendChatMessage: this.client.sendChatMessage,
116+
chatMessages: this.client.chatMessages,
115117
});
116118
}
117119

src/client/react.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type ExposedClientProps<G extends any = any> = Pick<
3333
| 'playerID'
3434
| 'matchID'
3535
| 'matchData'
36+
| 'sendChatMessage'
3637
>;
3738

3839
export type BoardProps<G extends any = any> = ClientState<G> &
@@ -180,6 +181,8 @@ export function Client<
180181
redo: this.client.redo,
181182
log: this.client.log,
182183
matchData: this.client.matchData,
184+
sendChatMessage: this.client.sendChatMessage,
185+
chatMessages: this.client.chatMessages
183186
});
184187
}
185188

src/client/transport/local.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ describe('LocalTransport', () => {
303303
const master = ({
304304
onSync: jest.fn(),
305305
onUpdate: jest.fn(),
306+
onChatMessage: jest.fn(),
306307
} as unknown) as LocalMaster;
307308
class WrappedLocalTransport extends LocalTransport {
308309
setStore(store: Store) {
@@ -358,5 +359,12 @@ describe('LocalTransport', () => {
358359
null
359360
);
360361
});
362+
363+
test('send chat-message', () => {
364+
m.onChatMessage('matchID', { message: 'foo' });
365+
expect(m.master.onChatMessage).lastCalledWith('matchID', {
366+
message: 'foo',
367+
});
368+
});
361369
});
362370
});

src/client/transport/local.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as ActionCreators from '../../core/action-creators';
1010
import { InMemory } from '../../server/db/inmemory';
1111
import { LocalStorage } from '../../server/db/localstorage';
1212
import { Master, TransportAPI } from '../../master/master';
13-
import { Transport, TransportOpts } from './transport';
13+
import { Transport, TransportOpts, ChatCallback } from './transport';
1414
import {
1515
CredentialedActionShape,
1616
Game,
@@ -136,6 +136,7 @@ type LocalTransportOpts = TransportOpts & {
136136
*/
137137
export class LocalTransport extends Transport {
138138
master: LocalMaster;
139+
chatMessageCallback: ChatCallback;
139140

140141
/**
141142
* Creates a new Mutiplayer instance.
@@ -158,6 +159,10 @@ export class LocalTransport extends Transport {
158159
this.isConnected = true;
159160
}
160161

162+
onChatMessage(matchID, chatMessage) {
163+
this.master.onChatMessage(matchID, chatMessage);
164+
}
165+
161166
/**
162167
* Called when another player makes a move and the
163168
* master broadcasts the update to other clients (including
@@ -202,6 +207,10 @@ export class LocalTransport extends Transport {
202207
if (type == 'update') {
203208
this.onUpdate.apply(this, args);
204209
}
210+
if (type == 'chat') {
211+
const [matchID, message] = args;
212+
this.chatMessageCallback.apply(this, [message]);
213+
}
205214
});
206215
this.master.onSync(
207216
this.matchID,
@@ -223,6 +232,10 @@ export class LocalTransport extends Transport {
223232

224233
subscribeMatchData() {}
225234

235+
subscribeChatMessage(fn: ChatCallback) {
236+
this.chatMessageCallback = fn;
237+
}
238+
226239
/**
227240
* Dispatches a reset action, then requests a fresh sync from the master.
228241
*/

src/client/transport/socketio.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,23 @@ describe('multiplayer', () => {
192192
const args: UpdateArgs = [action, state._stateID, 'default', null];
193193
expect(mockSocket.emit).lastCalledWith('update', ...args);
194194
});
195+
196+
test('receive chat-message', () => {
197+
let receivedChatData;
198+
m.subscribeChatMessage(data => (receivedChatData = data));
199+
const chatData = { message: 'foo' };
200+
mockSocket.receive('chat', 'unknown matchID', chatData);
201+
expect(receivedChatData).toBe(undefined);
202+
mockSocket.receive('chat', 'default', chatData);
203+
expect(receivedChatData).toMatchObject(receivedChatData);
204+
});
205+
206+
test('send chat-message', () => {
207+
m.onChatMessage('matchID', { message: 'foo' });
208+
expect(mockSocket.emit).lastCalledWith('chat', 'matchID', {
209+
message: 'foo',
210+
});
211+
});
195212
});
196213

197214
describe('server option', () => {

src/client/transport/socketio.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@ const io = ioNamespace.default;
1111

1212
import * as ActionCreators from '../../core/action-creators';
1313
import { Master } from '../../master/master';
14-
import { Transport, TransportOpts, MetadataCallback } from './transport';
14+
import {
15+
Transport,
16+
TransportOpts,
17+
MetadataCallback,
18+
ChatCallback,
19+
} from './transport';
1520
import {
1621
CredentialedActionShape,
1722
FilteredMetadata,
1823
LogEntry,
1924
PlayerID,
2025
State,
2126
SyncInfo,
27+
ChatMessage,
2228
} from '../../types';
2329

2430
interface SocketIOOpts {
@@ -42,6 +48,7 @@ export class SocketIOTransport extends Transport {
4248
socketOpts: SocketIOClient.ConnectOpts;
4349
callback: () => void;
4450
matchDataCallback: MetadataCallback;
51+
chatMessageCallback: ChatCallback;
4552

4653
/**
4754
* Creates a new Mutiplayer instance.
@@ -72,6 +79,7 @@ export class SocketIOTransport extends Transport {
7279
this.isConnected = false;
7380
this.callback = () => {};
7481
this.matchDataCallback = () => {};
82+
this.chatMessageCallback = () => {};
7583
}
7684

7785
/**
@@ -88,6 +96,10 @@ export class SocketIOTransport extends Transport {
8896
this.socket.emit('update', ...args);
8997
}
9098

99+
onChatMessage(matchID, chatMessage) {
100+
this.socket.emit('chat', matchID, chatMessage);
101+
}
102+
91103
/**
92104
* Connect to the server.
93105
*/
@@ -147,6 +159,12 @@ export class SocketIOTransport extends Transport {
147159
}
148160
);
149161

162+
this.socket.on('chat', (matchID: string, chatMessage: ChatMessage) => {
163+
if (matchID === this.matchID) {
164+
this.chatMessageCallback(chatMessage);
165+
}
166+
});
167+
150168
// Keep track of connection status.
151169
this.socket.on('connect', () => {
152170
// Initial sync to get game state.
@@ -181,6 +199,10 @@ export class SocketIOTransport extends Transport {
181199
this.matchDataCallback = fn;
182200
}
183201

202+
subscribeChatMessage(fn: ChatCallback) {
203+
this.chatMessageCallback = fn;
204+
}
205+
184206
/**
185207
* Send a “sync” event to the server.
186208
*/

0 commit comments

Comments
 (0)