diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 5420adc..700e52d 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -175,6 +175,16 @@ export default class RoomClient }); } + changeProfilePicture(picture) + { + logger.debug('changeProfilePicture() [picture: "%s"]', picture); + + this._protoo.send('change-profile-picture', { picture }).catch((error) => + { + logger.error('shareProfilePicure() | failed: %o', error); + }); + } + sendChatMessage(chatMessage) { logger.debug('sendChatMessage() [chatMessage:"%s"]', chatMessage); @@ -1052,7 +1062,18 @@ export default class RoomClient break; } - // This means: server wants to change MY displayName + case 'profile-picture-changed': + { + accept(); + + const { peerName, picture } = request.data; + + this._dispatch(stateActions.setPeerPicture(peerName, picture)); + + break; + } + + // This means: server wants to change MY user information case 'auth': { logger.debug('got auth event from server', request.data); @@ -1061,6 +1082,10 @@ export default class RoomClient if (request.data.verified == true) { this.changeDisplayName(request.data.name); + + this.changeProfilePicture(request.data.picture); + this._dispatch(stateActions.setPicture(request.data.picture)); + this._dispatch(requestActions.notify( { text : `Authenticated successfully: ${request.data}` @@ -1103,7 +1128,7 @@ export default class RoomClient logger.debug('Got chat from "%s"', peerName); this._dispatch( - stateActions.addResponseMessage(chatMessage)); + stateActions.addResponseMessage({ ...chatMessage, peerName })); break; } diff --git a/app/lib/components/Chat/Chat.jsx b/app/lib/components/Chat/Chat.jsx index e678a3d..898bf75 100644 --- a/app/lib/components/Chat/Chat.jsx +++ b/app/lib/components/Chat/Chat.jsx @@ -14,7 +14,8 @@ class Chat extends Component onSendMessage, disabledInput, autofocus, - displayName + displayName, + picture } = this.props; return ( @@ -22,7 +23,7 @@ class Chat extends Component
{ onSendMessage(e, displayName); }} + onSubmit={(e) => { onSendMessage(e, displayName, picture); }} > { return { disabledInput : state.chatbehavior.disabledInput, - displayName : state.me.displayName + displayName : state.me.displayName, + picture : state.me.picture }; }; const mapDispatchToProps = (dispatch) => { return { - onSendMessage : (event, displayName) => + onSendMessage : (event, displayName, picture) => { event.preventDefault(); const userInput = event.target.message.value; @@ -74,7 +77,7 @@ const mapDispatchToProps = (dispatch) => if (userInput) { dispatch(stateActions.addUserMessage(userInput)); - dispatch(requestActions.sendChatMessage(userInput, displayName)); + dispatch(requestActions.sendChatMessage(userInput, displayName, picture)); } event.target.message.value = ''; } diff --git a/app/lib/components/Chat/MessageList.jsx b/app/lib/components/Chat/MessageList.jsx index d0bbf7b..3f59d1a 100644 --- a/app/lib/components/Chat/MessageList.jsx +++ b/app/lib/components/Chat/MessageList.jsx @@ -50,20 +50,28 @@ class MessageList extends Component { const messageTime = new Date(message.time); + const picture = (message.sender === 'response' ? + message.picture : this.props.myPicture) || 'resources/images/avatar-empty.jpeg'; + return (
-
- - {message.name} - {this.getTimeString(messageTime)} - + + +
+
+ + + {message.name} - {this.getTimeString(messageTime)} + +
); @@ -76,13 +84,15 @@ class MessageList extends Component MessageList.propTypes = { - chatmessages : PropTypes.arrayOf(PropTypes.object).isRequired + chatmessages : PropTypes.arrayOf(PropTypes.object).isRequired, + myPicture : PropTypes.string }; const mapStateToProps = (state) => { return { - chatmessages : state.chatmessages + chatmessages : state.chatmessages, + myPicture : state.me.picture }; }; diff --git a/app/lib/components/ParticipantList/ListPeer.jsx b/app/lib/components/ParticipantList/ListPeer.jsx index 5702100..8308f79 100644 --- a/app/lib/components/ParticipantList/ListPeer.jsx +++ b/app/lib/components/ParticipantList/ListPeer.jsx @@ -38,9 +38,12 @@ const ListPeer = (props) => !screenConsumer.remotelyPaused ); + const picture = peer.picture || 'resources/images/avatar-empty.jpeg'; + return (
- + +
{peer.displayName}
diff --git a/app/lib/redux/reducers/chatmessages.js b/app/lib/redux/reducers/chatmessages.js index e95789e..020908e 100644 --- a/app/lib/redux/reducers/chatmessages.js +++ b/app/lib/redux/reducers/chatmessages.js @@ -3,9 +3,7 @@ import createNewMessage } from './helper'; -const initialState = []; - -const chatmessages = (state = initialState, action) => +const chatmessages = (state = [], action) => { switch (action.type) { diff --git a/app/lib/redux/reducers/helper.js b/app/lib/redux/reducers/helper.js index 1423eb1..db17859 100644 --- a/app/lib/redux/reducers/helper.js +++ b/app/lib/redux/reducers/helper.js @@ -1,10 +1,11 @@ -export function createNewMessage(text, sender, name) +export function createNewMessage(text, sender, name, picture) { return { type : 'message', text, time : Date.now(), name, - sender + sender, + picture }; -} +} \ No newline at end of file diff --git a/app/lib/redux/reducers/me.js b/app/lib/redux/reducers/me.js index 75e89ae..0b8db12 100644 --- a/app/lib/redux/reducers/me.js +++ b/app/lib/redux/reducers/me.js @@ -21,7 +21,8 @@ const initialState = audioOnlyInProgress : false, raiseHand : false, raiseHandInProgress : false, - restartIceInProgress : false + restartIceInProgress : false, + picture : null }; const me = (state = initialState, action) => @@ -164,6 +165,11 @@ const me = (state = initialState, action) => return { ...state, restartIceInProgress: flag }; } + case 'SET_PICTURE': + { + return { ...state, picture: action.payload.picture }; + } + default: return state; } diff --git a/app/lib/redux/reducers/peers.js b/app/lib/redux/reducers/peers.js index d7174d5..ea9cb44 100644 --- a/app/lib/redux/reducers/peers.js +++ b/app/lib/redux/reducers/peers.js @@ -1,126 +1,93 @@ -const initialState = {}; +import omit from 'lodash/omit'; -const peers = (state = initialState, action) => +const peer = (state = {}, action) => +{ + switch (action.type) + { + case 'ADD_PEER': + return action.payload.peer; + + case 'SET_PEER_DISPLAY_NAME': + return { ...state, displayName: action.payload.displayName }; + + case 'SET_PEER_VIDEO_IN_PROGRESS': + return { ...state, peerVideoInProgress: action.payload.flag }; + + case 'SET_PEER_AUDIO_IN_PROGRESS': + return { ...state, peerAudioInProgress: action.payload.flag }; + + case 'SET_PEER_SCREEN_IN_PROGRESS': + return { ...state, peerScreenInProgress: action.payload.flag }; + + case 'SET_PEER_RAISE_HAND_STATE': + return { ...state, raiseHandState: action.payload.raiseHandState }; + + case 'ADD_CONSUMER': + { + const consumers = [ ...state.consumers, action.payload.consumer.id ]; + + return { ...state, consumers }; + } + + case 'REMOVE_CONSUMER': + { + const consumers = state.consumers.filter((consumer) => + consumer !== action.payload.consumerId); + + return { ...state, consumers }; + } + + case 'SET_PEER_PICTURE': + { + return { ...state, picture: action.payload.picture }; + } + + default: + return state; + } +}; + +const peers = (state = {}, action) => { switch (action.type) { case 'ADD_PEER': { - const { peer } = action.payload; - - return { ...state, [peer.name]: peer }; + return { ...state, [action.payload.peer.name]: peer(undefined, action) }; } case 'REMOVE_PEER': { - const { peerName } = action.payload; - const newState = { ...state }; - - delete newState[peerName]; - - return newState; + return omit(state, [ action.payload.peerName ]); } case 'SET_PEER_DISPLAY_NAME': - { - const { displayName, peerName } = action.payload; - const peer = state[peerName]; - - if (!peer) - throw new Error('no Peer found'); - - const newPeer = { ...peer, displayName }; - - return { ...state, [newPeer.name]: newPeer }; - } - case 'SET_PEER_VIDEO_IN_PROGRESS': - { - const { peerName, flag } = action.payload; - const peer = state[peerName]; - - if (!peer) - throw new Error('no Peer found'); - - const newPeer = { ...peer, peerVideoInProgress: flag }; - - return { ...state, [newPeer.name]: newPeer }; - } - case 'SET_PEER_AUDIO_IN_PROGRESS': - { - const { peerName, flag } = action.payload; - const peer = state[peerName]; - - if (!peer) - throw new Error('no Peer found'); - - const newPeer = { ...peer, peerAudioInProgress: flag }; - - return { ...state, [newPeer.name]: newPeer }; - } - case 'SET_PEER_SCREEN_IN_PROGRESS': - { - const { peerName, flag } = action.payload; - const peer = state[peerName]; - - if (!peer) - throw new Error('no Peer found'); - - const newPeer = { ...peer, peerScreenInProgress: flag }; - - return { ...state, [newPeer.name]: newPeer }; - } - case 'SET_PEER_RAISE_HAND_STATE': - { - const { peerName, raiseHandState } = action.payload; - const peer = state[peerName]; - - if (!peer) - throw new Error('no Peer found'); - - const newPeer = { ...peer, raiseHandState }; - - return { ...state, [newPeer.name]: newPeer }; - } - + case 'SET_PEER_PICTURE': case 'ADD_CONSUMER': { - const { consumer, peerName } = action.payload; - const peer = state[peerName]; + const oldPeer = state[action.payload.peerName]; - if (!peer) - throw new Error('no Peer found for new Consumer'); + if (!oldPeer) + { + throw new Error('no Peer found'); + } - const newConsumers = [ ...peer.consumers, consumer.id ]; - const newPeer = { ...peer, consumers: newConsumers }; - - return { ...state, [newPeer.name]: newPeer }; + return { ...state, [oldPeer.name]: peer(oldPeer, action) }; } - + case 'REMOVE_CONSUMER': { - const { consumerId, peerName } = action.payload; - const peer = state[peerName]; + const oldPeer = state[action.payload.peerName]; // NOTE: This means that the Peer was closed before, so it's ok. - if (!peer) + if (!oldPeer) return state; - const idx = peer.consumers.indexOf(consumerId); - - if (idx === -1) - throw new Error('Consumer not found'); - - const newConsumers = peer.consumers.slice(); - - newConsumers.splice(idx, 1); - - const newPeer = { ...peer, consumers: newConsumers }; - - return { ...state, [newPeer.name]: newPeer }; + return { ...state, [oldPeer.name]: peer(oldPeer, action) }; } default: diff --git a/app/lib/redux/requestActions.js b/app/lib/redux/requestActions.js index 5d9a240..6d54490 100644 --- a/app/lib/redux/requestActions.js +++ b/app/lib/redux/requestActions.js @@ -184,9 +184,9 @@ export const installExtension = () => }; }; -export const sendChatMessage = (text, name) => +export const sendChatMessage = (text, name, picture) => { - const message = createNewMessage(text, 'response', name); + const message = createNewMessage(text, 'response', name, picture); return { type : 'SEND_CHAT_MESSAGE', diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 62307a8..9dcbb41 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -434,3 +434,15 @@ export const dropMessages = () => type : 'DROP_MESSAGES' }; }; + +export const setPicture = (picture) => + ({ + type : 'SET_PICTURE', + payload : { picture } + }); + +export const setPeerPicture = (peerName, picture) => + ({ + type : 'SET_PEER_PICTURE', + payload : { peerName, picture } + }); \ No newline at end of file diff --git a/app/resources/images/avatar-empty.jpeg b/app/resources/images/avatar-empty.jpeg new file mode 100644 index 0000000..56c4679 Binary files /dev/null and b/app/resources/images/avatar-empty.jpeg differ diff --git a/app/stylus/components/Chat.styl b/app/stylus/components/Chat.styl index e7adc4e..0541e70 100644 --- a/app/stylus/components/Chat.styl +++ b/app/stylus/components/Chat.styl @@ -73,38 +73,33 @@ word-wrap: break-word; > .client { - background-color: rgba(#000, 0.1); - border-radius: 5px; - padding: 6px; - max-width: 215px; - text-align: left; margin-left: auto; - - > .message-text { - font-size: 1.3vmin; - } - - > .message-time { - font-size: 1vmin; - opacity:0.8; - } } - > .response { + > .client, > .response { background-color: rgba(#000, 0.1); border-radius: 5px; - padding: 6px; max-width: 215px; - text-align: left; - font-size: 1.3vmin; + display: flex; + align-items: center; + padding: 6px; - > .message-text { - font-size: 1.3vmin; + > .message-avatar { + height: 2rem; + border-radius: 50%; } - > .message-time { - font-size: 1vmin; - opacity: 0.8; + > .message-content { + padding-left: 6px; + + > .message-text { + font-size: 1.3vmin; + } + + > .message-time { + font-size: 1vmin; + opacity: 0.8; + } } } } diff --git a/app/stylus/components/ParticipantList.styl b/app/stylus/components/ParticipantList.styl index 9481fad..7f4328a 100644 --- a/app/stylus/components/ParticipantList.styl +++ b/app/stylus/components/ParticipantList.styl @@ -15,6 +15,8 @@ } [data-component='ListPeer'] { + display: flex; + > .controls { float: right; display: flex; @@ -110,22 +112,16 @@ } > .avatar { - padding: 8px 16px; - float: left; - width: auto; - border: none; - display: block; - outline: 0; - border-radius: 50%; - vertical-align: middle; + border-radius: 50%; + height: 2rem; } > .peer-info { - font-size: 1.4vmin; - float: left; - width: auto; - border: none; - outline: 0; - padding: 1vmin; + font-size: 1.4vmin; + border: none; + display: flex; + padding: 1vmin; + flex-grow: 1; + align-items: center; } -} +} \ No newline at end of file diff --git a/server/lib/Room.js b/server/lib/Room.js index c6b5089..1f6d13c 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -228,6 +228,18 @@ class Room extends EventEmitter break; } + case 'change-profile-picture': + { + accept(); + + this._protooRoom.spread('profile-picture-changed', { + peerName: protooPeer.id, + picture: request.data.picture + }, [ protooPeer ]); + + break; + } + case 'chat-message': { accept();