Add ability to set nickname using /nick command in chat
This commit is contained in:
parent
54659863b5
commit
ba92214296
20
src/client/actions/NicknameActions.ts
Normal file
20
src/client/actions/NicknameActions.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { NICKNAME_SET } from '../constants'
|
||||||
|
|
||||||
|
interface NicknameSetPayload {
|
||||||
|
nickname: string
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NicknameSetAction {
|
||||||
|
type: 'NICKNAME_SET'
|
||||||
|
payload: NicknameSetPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNickname(payload: NicknameSetPayload): NicknameSetAction {
|
||||||
|
return {
|
||||||
|
type: NICKNAME_SET,
|
||||||
|
payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NicknameActions = NicknameSetAction
|
||||||
@ -7,6 +7,7 @@ import { EventEmitter } from 'events'
|
|||||||
import { createStore, Store, GetState } from '../store'
|
import { createStore, Store, GetState } from '../store'
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
import { ClientSocket } from '../socket'
|
import { ClientSocket } from '../socket'
|
||||||
|
import { PEERCALLS, PEER_EVENT_DATA, ME } from '../constants'
|
||||||
|
|
||||||
describe('PeerActions', () => {
|
describe('PeerActions', () => {
|
||||||
function createSocket () {
|
function createSocket () {
|
||||||
@ -74,20 +75,30 @@ describe('PeerActions', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('events', () => {
|
describe('events', () => {
|
||||||
let peer: Peer.Instance
|
function createPeer() {
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
PeerActions.createPeer({ socket, user, initiator: 'user1', stream })(
|
PeerActions.createPeer({ socket, user, initiator: 'user1', stream })(
|
||||||
dispatch, getState)
|
dispatch, getState)
|
||||||
peer = instances[0]
|
const peer = instances[instances.length - 1]
|
||||||
})
|
return peer
|
||||||
|
}
|
||||||
|
|
||||||
describe('connect', () => {
|
describe('connect', () => {
|
||||||
beforeEach(() => peer.emit('connect'))
|
|
||||||
|
|
||||||
it('dispatches peer connection established message', () => {
|
it('dispatches peer connection established message', () => {
|
||||||
|
createPeer().emit('connect')
|
||||||
// TODO
|
// TODO
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sends existing local streams to new peer', () => {
|
||||||
|
PeerActions.sendMessage({
|
||||||
|
payload: {nickname: 'john'},
|
||||||
|
type: 'nickname',
|
||||||
|
})(dispatch, getState)
|
||||||
|
const peer = createPeer()
|
||||||
|
peer.emit('connect')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends current nickname to new peer', () => {
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('data', () => {
|
describe('data', () => {
|
||||||
@ -103,10 +114,15 @@ describe('PeerActions', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('decodes a message', () => {
|
it('decodes a message', () => {
|
||||||
const payload = 'test'
|
const peer = createPeer()
|
||||||
const object = JSON.stringify({ payload })
|
const message = {
|
||||||
|
type: 'text',
|
||||||
|
payload: 'test',
|
||||||
|
}
|
||||||
|
const object = JSON.stringify(message)
|
||||||
peer.emit('data', Buffer.from(object, 'utf-8'))
|
peer.emit('data', Buffer.from(object, 'utf-8'))
|
||||||
const { list } = store.getState().messages
|
const { list } = store.getState().messages
|
||||||
|
expect(list.length).toBeGreaterThan(0)
|
||||||
expect(list[list.length - 1]).toEqual({
|
expect(list[list.length - 1]).toEqual({
|
||||||
userId: 'user2',
|
userId: 'user2',
|
||||||
timestamp: jasmine.any(String),
|
timestamp: jasmine.any(String),
|
||||||
@ -162,7 +178,7 @@ describe('PeerActions', () => {
|
|||||||
})(dispatch, getState)
|
})(dispatch, getState)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends a message to all peers', () => {
|
it('sends a text message to all peers', () => {
|
||||||
PeerActions.sendMessage({ payload: 'test', type: 'text' })(
|
PeerActions.sendMessage({ payload: 'test', type: 'text' })(
|
||||||
dispatch, getState)
|
dispatch, getState)
|
||||||
const { peers } = store.getState()
|
const { peers } = store.getState()
|
||||||
@ -172,5 +188,76 @@ describe('PeerActions', () => {
|
|||||||
.toEqual([[ '{"payload":"test","type":"text"}' ]])
|
.toEqual([[ '{"payload":"test","type":"text"}' ]])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sends a nickname change to all peers', () => {
|
||||||
|
PeerActions.sendMessage({
|
||||||
|
payload: {nickname: 'john'},
|
||||||
|
type: 'nickname',
|
||||||
|
})(dispatch, getState)
|
||||||
|
const { nicknames, peers } = store.getState()
|
||||||
|
expect((peers['user2'].send as jest.Mock).mock.calls)
|
||||||
|
.toEqual([[ '{"payload":{"nickname":"john"},"type":"nickname"}' ]])
|
||||||
|
expect((peers['user3'].send as jest.Mock).mock.calls)
|
||||||
|
.toEqual([[ '{"payload":{"nickname":"john"},"type":"nickname"}' ]])
|
||||||
|
expect(nicknames[ME]).toBe('john')
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('receive message (handleData)', () => {
|
||||||
|
let peer: Peer.Instance
|
||||||
|
function emitData(message: PeerActions.Message) {
|
||||||
|
peer.emit(PEER_EVENT_DATA, JSON.stringify(message))
|
||||||
|
}
|
||||||
|
beforeEach(() => {
|
||||||
|
PeerActions.createPeer({
|
||||||
|
socket, user: { id: 'user2' }, initiator: 'user2', stream,
|
||||||
|
})(dispatch, getState)
|
||||||
|
peer = store.getState().peers['user2']
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles a message', () => {
|
||||||
|
emitData({
|
||||||
|
payload: 'hello',
|
||||||
|
type: 'text',
|
||||||
|
})
|
||||||
|
expect(store.getState().messages.list)
|
||||||
|
.toEqual([{
|
||||||
|
message: 'Connecting to peer...',
|
||||||
|
userId: PEERCALLS,
|
||||||
|
timestamp: jasmine.any(String),
|
||||||
|
}, {
|
||||||
|
message: 'hello',
|
||||||
|
userId: 'user2',
|
||||||
|
image: undefined,
|
||||||
|
timestamp: jasmine.any(String),
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles nickname changes', () => {
|
||||||
|
emitData({
|
||||||
|
payload: {nickname: 'john'},
|
||||||
|
type: 'nickname',
|
||||||
|
})
|
||||||
|
emitData({
|
||||||
|
payload: {nickname: 'john2'},
|
||||||
|
type: 'nickname',
|
||||||
|
})
|
||||||
|
expect(store.getState().messages.list)
|
||||||
|
.toEqual([{
|
||||||
|
message: 'Connecting to peer...',
|
||||||
|
userId: PEERCALLS,
|
||||||
|
timestamp: jasmine.any(String),
|
||||||
|
}, {
|
||||||
|
message: 'User user2 is now known as john',
|
||||||
|
userId: PEERCALLS,
|
||||||
|
image: undefined,
|
||||||
|
timestamp: jasmine.any(String),
|
||||||
|
}, {
|
||||||
|
message: 'User john is now known as john2',
|
||||||
|
userId: PEERCALLS,
|
||||||
|
image: undefined,
|
||||||
|
timestamp: jasmine.any(String),
|
||||||
|
}])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import * as ChatActions from '../actions/ChatActions'
|
import * as ChatActions from './ChatActions'
|
||||||
import * as NotifyActions from '../actions/NotifyActions'
|
import * as NicknameActions from './NicknameActions'
|
||||||
import * as StreamActions from '../actions/StreamActions'
|
import * as NotifyActions from './NotifyActions'
|
||||||
|
import * as StreamActions from './StreamActions'
|
||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
import Peer, { SignalData } from 'simple-peer'
|
import Peer, { SignalData } from 'simple-peer'
|
||||||
import forEach from 'lodash/forEach'
|
import forEach from 'lodash/forEach'
|
||||||
@ -8,6 +9,7 @@ import _debug from 'debug'
|
|||||||
import { iceServers } from '../window'
|
import { iceServers } from '../window'
|
||||||
import { Dispatch, GetState } from '../store'
|
import { Dispatch, GetState } from '../store'
|
||||||
import { ClientSocket } from '../socket'
|
import { ClientSocket } from '../socket'
|
||||||
|
import { getNickname } from '../nickname'
|
||||||
|
|
||||||
const debug = _debug('peercalls')
|
const debug = _debug('peercalls')
|
||||||
|
|
||||||
@ -65,6 +67,13 @@ class PeerHandler {
|
|||||||
peer.addTrack(track, s.stream)
|
peer.addTrack(track, s.stream)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
const nickname = state.nicknames[constants.ME]
|
||||||
|
if (nickname) {
|
||||||
|
sendData(peer, {
|
||||||
|
payload: {nickname},
|
||||||
|
type: 'nickname',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handleTrack = (track: MediaStreamTrack, stream: MediaStream) => {
|
handleTrack = (track: MediaStreamTrack, stream: MediaStream) => {
|
||||||
const { user, dispatch } = this
|
const { user, dispatch } = this
|
||||||
@ -86,7 +95,8 @@ class PeerHandler {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
handleData = (buffer: ArrayBuffer) => {
|
handleData = (buffer: ArrayBuffer) => {
|
||||||
const { dispatch, user } = this
|
const { dispatch, getState, user } = this
|
||||||
|
const state = getState()
|
||||||
const message = JSON.parse(new window.TextDecoder('utf-8').decode(buffer))
|
const message = JSON.parse(new window.TextDecoder('utf-8').decode(buffer))
|
||||||
debug('peer: %s, message: %o', user.id, buffer)
|
debug('peer: %s, message: %o', user.id, buffer)
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
@ -98,6 +108,19 @@ class PeerHandler {
|
|||||||
image: message.payload.data,
|
image: message.payload.data,
|
||||||
}))
|
}))
|
||||||
break
|
break
|
||||||
|
case 'nickname':
|
||||||
|
dispatch(ChatActions.addMessage({
|
||||||
|
userId: constants.PEERCALLS,
|
||||||
|
message: 'User ' + getNickname(state.nicknames, user.id) +
|
||||||
|
' is now known as ' + message.payload.nickname,
|
||||||
|
timestamp: new Date().toLocaleString(),
|
||||||
|
image: undefined,
|
||||||
|
}))
|
||||||
|
dispatch(NicknameActions.setNickname({
|
||||||
|
userId: user.id,
|
||||||
|
nickname: message.payload.nickname,
|
||||||
|
}))
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
dispatch(ChatActions.addMessage({
|
dispatch(ChatActions.addMessage({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -234,7 +257,18 @@ export interface FileMessage {
|
|||||||
payload: Base64File
|
payload: Base64File
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Message = TextMessage | FileMessage
|
export interface NicknameMessage {
|
||||||
|
type: 'nickname'
|
||||||
|
payload: {
|
||||||
|
nickname: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Message = TextMessage | FileMessage | NicknameMessage
|
||||||
|
|
||||||
|
function sendData(peer: Peer.Instance, message: Message) {
|
||||||
|
peer.send(JSON.stringify(message))
|
||||||
|
}
|
||||||
|
|
||||||
export const sendMessage = (message: Message) =>
|
export const sendMessage = (message: Message) =>
|
||||||
(dispatch: Dispatch, getState: GetState) => {
|
(dispatch: Dispatch, getState: GetState) => {
|
||||||
@ -244,23 +278,37 @@ export const sendMessage = (message: Message) =>
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'file':
|
case 'file':
|
||||||
dispatch(ChatActions.addMessage({
|
dispatch(ChatActions.addMessage({
|
||||||
userId: 'You',
|
userId: constants.ME,
|
||||||
message: 'Send file: "' +
|
message: 'Send file: "' +
|
||||||
message.payload.name + '" to all peers',
|
message.payload.name + '" to all peers',
|
||||||
timestamp: new Date().toLocaleString(),
|
timestamp: new Date().toLocaleString(),
|
||||||
image: message.payload.data,
|
image: message.payload.data,
|
||||||
}))
|
}))
|
||||||
break
|
break
|
||||||
|
case 'nickname':
|
||||||
|
dispatch(ChatActions.addMessage({
|
||||||
|
userId: constants.PEERCALLS,
|
||||||
|
message: 'You are now known as: ' + message.payload.nickname,
|
||||||
|
timestamp: new Date().toLocaleString(),
|
||||||
|
image: undefined,
|
||||||
|
}))
|
||||||
|
dispatch(NicknameActions.setNickname({
|
||||||
|
userId: constants.ME,
|
||||||
|
nickname: message.payload.nickname,
|
||||||
|
}))
|
||||||
|
window.localStorage &&
|
||||||
|
(window.localStorage.nickname = message.payload.nickname)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
dispatch(ChatActions.addMessage({
|
dispatch(ChatActions.addMessage({
|
||||||
userId: 'You',
|
userId: constants.ME,
|
||||||
message: message.payload,
|
message: message.payload,
|
||||||
timestamp: new Date().toLocaleString(),
|
timestamp: new Date().toLocaleString(),
|
||||||
image: undefined,
|
image: undefined,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
forEach(peers, (peer, userId) => {
|
forEach(peers, (peer, userId) => {
|
||||||
peer.send(JSON.stringify(message))
|
sendData(peer, message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React from 'react'
|
|||||||
import Peer from 'simple-peer'
|
import Peer from 'simple-peer'
|
||||||
import { Message } from '../actions/ChatActions'
|
import { Message } from '../actions/ChatActions'
|
||||||
import { dismissNotification, Notification } from '../actions/NotifyActions'
|
import { dismissNotification, Notification } from '../actions/NotifyActions'
|
||||||
import { TextMessage } from '../actions/PeerActions'
|
import { Message as MessageType } from '../actions/PeerActions'
|
||||||
import { removeStream } from '../actions/StreamActions'
|
import { removeStream } from '../actions/StreamActions'
|
||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
import Chat from './Chat'
|
import Chat from './Chat'
|
||||||
@ -15,17 +15,19 @@ import Toolbar from './Toolbar'
|
|||||||
import Video from './Video'
|
import Video from './Video'
|
||||||
import { getDesktopStream } from '../actions/MediaActions'
|
import { getDesktopStream } from '../actions/MediaActions'
|
||||||
import { StreamsState } from '../reducers/streams'
|
import { StreamsState } from '../reducers/streams'
|
||||||
|
import { Nicknames } from '../reducers/nicknames'
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
active: string | null
|
active: string | null
|
||||||
dismissNotification: typeof dismissNotification
|
dismissNotification: typeof dismissNotification
|
||||||
init: () => void
|
init: () => void
|
||||||
|
nicknames: Nicknames
|
||||||
notifications: Record<string, Notification>
|
notifications: Record<string, Notification>
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
messagesCount: number
|
messagesCount: number
|
||||||
peers: Record<string, Peer.Instance>
|
peers: Record<string, Peer.Instance>
|
||||||
play: () => void
|
play: () => void
|
||||||
sendMessage: (message: TextMessage) => void
|
sendMessage: (message: MessageType) => void
|
||||||
streams: StreamsState
|
streams: StreamsState
|
||||||
getDesktopStream: typeof getDesktopStream
|
getDesktopStream: typeof getDesktopStream
|
||||||
removeStream: typeof removeStream
|
removeStream: typeof removeStream
|
||||||
@ -79,6 +81,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
active,
|
active,
|
||||||
dismissNotification,
|
dismissNotification,
|
||||||
notifications,
|
notifications,
|
||||||
|
nicknames,
|
||||||
messages,
|
messages,
|
||||||
messagesCount,
|
messagesCount,
|
||||||
onSendFile,
|
onSendFile,
|
||||||
@ -127,6 +130,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
</Side>
|
</Side>
|
||||||
<Chat
|
<Chat
|
||||||
messages={messages}
|
messages={messages}
|
||||||
|
nicknames={nicknames}
|
||||||
onClose={this.handleHideChat}
|
onClose={this.handleHideChat}
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
visible={this.state.chatVisible}
|
visible={this.state.chatVisible}
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Message as MessageType } from '../actions/ChatActions'
|
import { Message as ChatMessage } from '../actions/ChatActions'
|
||||||
import { TextMessage } from '../actions/PeerActions'
|
import { Message } from '../actions/PeerActions'
|
||||||
|
import { Nicknames } from '../reducers/nicknames'
|
||||||
import Input from './Input'
|
import Input from './Input'
|
||||||
|
import { ME } from '../constants'
|
||||||
|
import { getNickname } from '../nickname'
|
||||||
|
|
||||||
export interface MessageProps {
|
export interface MessageProps {
|
||||||
message: MessageType
|
message: ChatMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
function Message (props: MessageProps) {
|
function MessageEntry (props: MessageProps) {
|
||||||
const { message } = props
|
const { message } = props
|
||||||
return (
|
return (
|
||||||
<p className="message-text">
|
<p className="message-text">
|
||||||
@ -22,9 +25,10 @@ function Message (props: MessageProps) {
|
|||||||
|
|
||||||
export interface ChatProps {
|
export interface ChatProps {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
messages: MessageType[]
|
messages: ChatMessage[]
|
||||||
|
nicknames: Nicknames
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
sendMessage: (message: TextMessage) => void
|
sendMessage: (message: Message) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Chat extends React.PureComponent<ChatProps> {
|
export default class Chat extends React.PureComponent<ChatProps> {
|
||||||
@ -67,15 +71,15 @@ export default class Chat extends React.PureComponent<ChatProps> {
|
|||||||
{messages.length ? (
|
{messages.length ? (
|
||||||
messages.map((message, i) => (
|
messages.map((message, i) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
{message.userId === 'You' ? (
|
{message.userId === ME ? (
|
||||||
<div className="chat-item chat-item-me">
|
<div className="chat-item chat-item-me">
|
||||||
<div className="message">
|
<div className="message">
|
||||||
<span className="message-user-name">
|
<span className="message-user-name">
|
||||||
{message.userId}
|
{getNickname(this.props.nicknames, message.userId)}
|
||||||
</span>
|
</span>
|
||||||
<span className="icon icon-schedule" />
|
<span className="icon icon-schedule" />
|
||||||
<time className="message-time">{message.timestamp}</time>
|
<time className="message-time">{message.timestamp}</time>
|
||||||
<Message message={message} />
|
<MessageEntry message={message} />
|
||||||
</div>
|
</div>
|
||||||
{message.image ? (
|
{message.image ? (
|
||||||
<img className="chat-item-img" src={message.image} />
|
<img className="chat-item-img" src={message.image} />
|
||||||
@ -92,11 +96,11 @@ export default class Chat extends React.PureComponent<ChatProps> {
|
|||||||
)}
|
)}
|
||||||
<div className="message">
|
<div className="message">
|
||||||
<span className="message-user-name">
|
<span className="message-user-name">
|
||||||
{message.userId}
|
{getNickname(this.props.nicknames, message.userId)}
|
||||||
</span>
|
</span>
|
||||||
<span className="icon icon-schedule" />
|
<span className="icon icon-schedule" />
|
||||||
<time className="message-time">{message.timestamp}</time>
|
<time className="message-time">{message.timestamp}</time>
|
||||||
<Message message={message} />
|
<MessageEntry message={message} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import Input from './Input'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import { TextMessage } from '../actions/PeerActions'
|
import { Message } from '../actions/PeerActions'
|
||||||
|
|
||||||
describe('components/Input', () => {
|
describe('components/Input', () => {
|
||||||
|
|
||||||
let node: Element
|
let node: Element
|
||||||
let sendMessage: jest.Mock<(message: TextMessage) => void>
|
let sendMessage: jest.MockedFunction<(message: Message) => void>
|
||||||
async function render () {
|
async function render () {
|
||||||
sendMessage = jest.fn()
|
sendMessage = jest.fn()
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
@ -32,23 +32,61 @@ describe('components/Input', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sendMessage.mockClear()
|
sendMessage.mockClear()
|
||||||
input = node.querySelector('textarea')!
|
input = node.querySelector('textarea')!
|
||||||
TestUtils.Simulate.change(input, {
|
|
||||||
target: { value: message } as any,
|
|
||||||
})
|
|
||||||
expect(input.value).toBe(message)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('handleSubmit', () => {
|
describe('handleSubmit', () => {
|
||||||
|
it('does nothing when no message', () => {
|
||||||
|
TestUtils.Simulate.change(input, {
|
||||||
|
target: { value: '' } as any,
|
||||||
|
})
|
||||||
|
TestUtils.Simulate.submit(node)
|
||||||
|
expect(sendMessage.mock.calls)
|
||||||
|
.toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
it('sends a message', () => {
|
it('sends a message', () => {
|
||||||
|
TestUtils.Simulate.change(input, {
|
||||||
|
target: { value: message } as any,
|
||||||
|
})
|
||||||
TestUtils.Simulate.submit(node)
|
TestUtils.Simulate.submit(node)
|
||||||
expect(input.value).toBe('')
|
expect(input.value).toBe('')
|
||||||
expect(sendMessage.mock.calls)
|
expect(sendMessage.mock.calls)
|
||||||
.toEqual([[ { payload: message, type: 'text' } ]])
|
.toEqual([[ { payload: message, type: 'text' } ]])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sends a nickname command', () => {
|
||||||
|
TestUtils.Simulate.change(input, {
|
||||||
|
target: { value: '/nick john' } as any,
|
||||||
|
})
|
||||||
|
TestUtils.Simulate.submit(node)
|
||||||
|
expect(sendMessage.mock.calls)
|
||||||
|
.toEqual([[ { payload: {nickname: 'john'}, type: 'nickname' } ]])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not fail when command is empty', () => {
|
||||||
|
TestUtils.Simulate.change(input, {
|
||||||
|
target: { value: '/nick ' } as any,
|
||||||
|
})
|
||||||
|
TestUtils.Simulate.submit(node)
|
||||||
|
expect(sendMessage.mock.calls)
|
||||||
|
.toEqual([[ { payload: {nickname: ''}, type: 'nickname' } ]])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends message when command is invalid', () => {
|
||||||
|
TestUtils.Simulate.change(input, {
|
||||||
|
target: { value: '/nick' } as any,
|
||||||
|
})
|
||||||
|
TestUtils.Simulate.submit(node)
|
||||||
|
expect(sendMessage.mock.calls)
|
||||||
|
.toEqual([[ { payload: '/nick', type: 'text' } ]])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('handleKeyPress', () => {
|
describe('handleKeyPress', () => {
|
||||||
it('sends a message', () => {
|
it('sends a message', () => {
|
||||||
|
TestUtils.Simulate.change(input, {
|
||||||
|
target: { value: message } as any,
|
||||||
|
})
|
||||||
TestUtils.Simulate.keyPress(input, {
|
TestUtils.Simulate.keyPress(input, {
|
||||||
key: 'Enter',
|
key: 'Enter',
|
||||||
})
|
})
|
||||||
@ -67,6 +105,9 @@ describe('components/Input', () => {
|
|||||||
|
|
||||||
describe('handleSmileClick', () => {
|
describe('handleSmileClick', () => {
|
||||||
it('adds smile to message', () => {
|
it('adds smile to message', () => {
|
||||||
|
TestUtils.Simulate.change(input, {
|
||||||
|
target: { value: message } as any,
|
||||||
|
})
|
||||||
const div = node.querySelector('.chat-controls-buttons-smile')!
|
const div = node.querySelector('.chat-controls-buttons-smile')!
|
||||||
TestUtils.Simulate.click(div)
|
TestUtils.Simulate.click(div)
|
||||||
expect(input.value).toBe('test message😑')
|
expect(input.value).toBe('test message😑')
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import React, { ReactEventHandler, ChangeEventHandler, KeyboardEventHandler, MouseEventHandler } from 'react'
|
import React, { ReactEventHandler, ChangeEventHandler, KeyboardEventHandler, MouseEventHandler } from 'react'
|
||||||
import { TextMessage } from '../actions/PeerActions'
|
import { Message } from '../actions/PeerActions'
|
||||||
|
|
||||||
export interface InputProps {
|
export interface InputProps {
|
||||||
sendMessage: (message: TextMessage) => void
|
sendMessage: (message: Message) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InputState {
|
export interface InputState {
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const regexp = /^\/([a-z0-9]+) (.*)$/
|
||||||
|
|
||||||
export default class Input extends React.PureComponent<InputProps, InputState> {
|
export default class Input extends React.PureComponent<InputProps, InputState> {
|
||||||
textArea = React.createRef<HTMLTextAreaElement>()
|
textArea = React.createRef<HTMLTextAreaElement>()
|
||||||
state = {
|
state = {
|
||||||
@ -38,10 +40,22 @@ export default class Input extends React.PureComponent<InputProps, InputState> {
|
|||||||
const { sendMessage } = this.props
|
const { sendMessage } = this.props
|
||||||
const { message } = this.state
|
const { message } = this.state
|
||||||
if (message) {
|
if (message) {
|
||||||
sendMessage({
|
const matches = regexp.exec(message)
|
||||||
payload: message,
|
const command = matches && matches[1]
|
||||||
type: 'text',
|
const restOfMessage = matches && matches[2] || ''
|
||||||
})
|
switch (command) {
|
||||||
|
case 'nick':
|
||||||
|
sendMessage({
|
||||||
|
type: 'nickname',
|
||||||
|
payload: {nickname: restOfMessage},
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
sendMessage({
|
||||||
|
payload: message,
|
||||||
|
type: 'text',
|
||||||
|
})
|
||||||
|
}
|
||||||
// let image = null
|
// let image = null
|
||||||
|
|
||||||
// // take snapshoot
|
// // take snapshoot
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export const ALERT_CLEAR = 'ALERT_CLEAR'
|
|||||||
export const INIT = 'INIT'
|
export const INIT = 'INIT'
|
||||||
|
|
||||||
export const ME = '_me_'
|
export const ME = '_me_'
|
||||||
|
export const PEERCALLS = '[PeerCalls]'
|
||||||
|
|
||||||
export const NOTIFY = 'NOTIFY'
|
export const NOTIFY = 'NOTIFY'
|
||||||
export const NOTIFY_DISMISS = 'NOTIFY_DISMISS'
|
export const NOTIFY_DISMISS = 'NOTIFY_DISMISS'
|
||||||
@ -21,6 +22,8 @@ export const MEDIA_VIDEO_CONSTRAINT_SET = 'MEDIA_VIDEO_CONSTRAINT_SET'
|
|||||||
export const MEDIA_AUDIO_CONSTRAINT_SET = 'MEDIA_AUDIO_CONSTRAINT_SET'
|
export const MEDIA_AUDIO_CONSTRAINT_SET = 'MEDIA_AUDIO_CONSTRAINT_SET'
|
||||||
export const MEDIA_PLAY = 'MEDIA_PLAY'
|
export const MEDIA_PLAY = 'MEDIA_PLAY'
|
||||||
|
|
||||||
|
export const NICKNAME_SET = 'NICKNAME_SET'
|
||||||
|
|
||||||
export const PEER_ADD = 'PEER_ADD'
|
export const PEER_ADD = 'PEER_ADD'
|
||||||
export const PEER_REMOVE = 'PEER_REMOVE'
|
export const PEER_REMOVE = 'PEER_REMOVE'
|
||||||
export const PEERS_DESTROY = 'PEERS_DESTROY'
|
export const PEERS_DESTROY = 'PEERS_DESTROY'
|
||||||
|
|||||||
@ -12,6 +12,7 @@ function mapStateToProps (state: State) {
|
|||||||
streams: state.streams,
|
streams: state.streams,
|
||||||
peers: state.peers,
|
peers: state.peers,
|
||||||
notifications: state.notifications,
|
notifications: state.notifications,
|
||||||
|
nicknames: state.nicknames,
|
||||||
messages: state.messages.list,
|
messages: state.messages.list,
|
||||||
messagesCount: state.messages.count,
|
messagesCount: state.messages.count,
|
||||||
active: state.active,
|
active: state.active,
|
||||||
|
|||||||
13
src/client/nickname.ts
Normal file
13
src/client/nickname.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Nicknames } from './reducers/nicknames'
|
||||||
|
import { ME } from './constants'
|
||||||
|
|
||||||
|
export function getNickname(nicknames: Nicknames, userId: string): string {
|
||||||
|
const nickname = nicknames[userId]
|
||||||
|
if (nickname) {
|
||||||
|
return nickname
|
||||||
|
}
|
||||||
|
if (userId === ME) {
|
||||||
|
return 'You'
|
||||||
|
}
|
||||||
|
return userId
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import messages from './messages'
|
|||||||
import peers from './peers'
|
import peers from './peers'
|
||||||
import media from './media'
|
import media from './media'
|
||||||
import streams from './streams'
|
import streams from './streams'
|
||||||
|
import nicknames from './nicknames'
|
||||||
import { combineReducers } from 'redux'
|
import { combineReducers } from 'redux'
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
@ -11,6 +12,7 @@ export default combineReducers({
|
|||||||
notifications,
|
notifications,
|
||||||
messages,
|
messages,
|
||||||
media,
|
media,
|
||||||
|
nicknames,
|
||||||
peers,
|
peers,
|
||||||
streams,
|
streams,
|
||||||
})
|
})
|
||||||
|
|||||||
27
src/client/reducers/nicknames.ts
Normal file
27
src/client/reducers/nicknames.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { NICKNAME_SET, PEER_REMOVE, ME } from '../constants'
|
||||||
|
import { NicknameActions } from '../actions/NicknameActions'
|
||||||
|
import { RemovePeerAction } from '../actions/PeerActions'
|
||||||
|
import omit = require('lodash/omit')
|
||||||
|
|
||||||
|
export type Nicknames = Record<string, string | undefined>
|
||||||
|
|
||||||
|
const defaultState: Nicknames = {
|
||||||
|
[ME]: localStorage && localStorage.nickname,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function nicknames(
|
||||||
|
state = defaultState,
|
||||||
|
action: NicknameActions | RemovePeerAction,
|
||||||
|
) {
|
||||||
|
switch (action.type) {
|
||||||
|
case PEER_REMOVE:
|
||||||
|
return omit(state, [action.payload.userId])
|
||||||
|
case NICKNAME_SET:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[action.payload.userId]: action.payload.nickname,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user