diff --git a/app/lib/components/appPropTypes.js b/app/lib/components/appPropTypes.js
index 30dc98e..0cd3dc3 100644
--- a/app/lib/components/appPropTypes.js
+++ b/app/lib/components/appPropTypes.js
@@ -69,3 +69,11 @@ export const Notification = PropTypes.shape(
type : PropTypes.oneOf([ 'info', 'error' ]).isRequired,
timeout : PropTypes.number
});
+
+export const Message = PropTypes.shape(
+ {
+ type : PropTypes.string,
+ component : PropTypes.string,
+ text : PropTypes.string,
+ sender : PropTypes.string
+ });
diff --git a/app/lib/redux/reducers/chatbehavior.js b/app/lib/redux/reducers/chatbehavior.js
new file mode 100644
index 0000000..bee0d85
--- /dev/null
+++ b/app/lib/redux/reducers/chatbehavior.js
@@ -0,0 +1,31 @@
+const initialState =
+{
+ showChat : false,
+ disabledInput : false,
+ badge : 0
+};
+
+const chatbehavior = (state = initialState, action) =>
+{
+ switch (action.type)
+ {
+ case 'TOGGLE_CHAT':
+ {
+ const showChat = !state.showChat;
+
+ return { ...state, showChat };
+ }
+
+ case 'TOGGLE_INPUT_DISABLED':
+ {
+ const disabledInput = !state.disabledInput;
+
+ return { ...state, disabledInput };
+ }
+
+ default:
+ return state;
+ }
+};
+
+export default chatbehavior;
diff --git a/app/lib/redux/reducers/chatmessages.js b/app/lib/redux/reducers/chatmessages.js
new file mode 100644
index 0000000..e95789e
--- /dev/null
+++ b/app/lib/redux/reducers/chatmessages.js
@@ -0,0 +1,45 @@
+import
+{
+ createNewMessage
+} from './helper';
+
+const initialState = [];
+
+const chatmessages = (state = initialState, action) =>
+{
+ switch (action.type)
+ {
+ case 'ADD_NEW_USER_MESSAGE':
+ {
+ const { text } = action.payload;
+
+ const message = createNewMessage(text, 'client', 'Me');
+
+ return [ ...state, message ];
+ }
+
+ case 'ADD_NEW_RESPONSE_MESSAGE':
+ {
+ const { message } = action.payload;
+
+ return [ ...state, message ];
+ }
+
+ case 'ADD_CHAT_HISTORY':
+ {
+ const { chatHistory } = action.payload;
+
+ return [ ...state, ...chatHistory ];
+ }
+
+ case 'DROP_MESSAGES':
+ {
+ return [];
+ }
+
+ default:
+ return state;
+ }
+};
+
+export default chatmessages;
diff --git a/app/lib/redux/reducers/helper.js b/app/lib/redux/reducers/helper.js
new file mode 100644
index 0000000..1423eb1
--- /dev/null
+++ b/app/lib/redux/reducers/helper.js
@@ -0,0 +1,10 @@
+export function createNewMessage(text, sender, name)
+{
+ return {
+ type : 'message',
+ text,
+ time : Date.now(),
+ name,
+ sender
+ };
+}
diff --git a/app/lib/redux/reducers/index.js b/app/lib/redux/reducers/index.js
index e71610b..779f775 100644
--- a/app/lib/redux/reducers/index.js
+++ b/app/lib/redux/reducers/index.js
@@ -5,6 +5,8 @@ import producers from './producers';
import peers from './peers';
import consumers from './consumers';
import notifications from './notifications';
+import chatmessages from './chatmessages';
+import chatbehavior from './chatbehavior';
const reducers = combineReducers(
{
@@ -13,7 +15,9 @@ const reducers = combineReducers(
producers,
peers,
consumers,
- notifications
+ notifications,
+ chatmessages,
+ chatbehavior
});
export default reducers;
diff --git a/app/lib/redux/requestActions.js b/app/lib/redux/requestActions.js
index 6fdf36e..14210ef 100644
--- a/app/lib/redux/requestActions.js
+++ b/app/lib/redux/requestActions.js
@@ -1,5 +1,9 @@
import randomString from 'random-string';
import * as stateActions from './stateActions';
+import
+{
+ createNewMessage
+} from './reducers/helper';
export const joinRoom = (
{ roomId, peerName, displayName, device, useSimulcast, produce }) =>
@@ -81,6 +85,16 @@ export const restartIce = () =>
};
};
+export const sendChatMessage = (text, name) =>
+{
+ const message = createNewMessage(text, 'response', name);
+
+ return {
+ type : 'SEND_CHAT_MESSAGE',
+ payload : { message }
+ };
+};
+
// This returns a redux-thunk action (a function).
export const notify = ({ type = 'info', text, timeout }) =>
{
diff --git a/app/lib/redux/roomClientMiddleware.js b/app/lib/redux/roomClientMiddleware.js
index b73b1af..271d700 100644
--- a/app/lib/redux/roomClientMiddleware.js
+++ b/app/lib/redux/roomClientMiddleware.js
@@ -108,6 +108,15 @@ export default ({ dispatch, getState }) => (next) =>
break;
}
+
+ case 'SEND_CHAT_MESSAGE':
+ {
+ const { message } = action.payload;
+
+ client.sendChatMessage(message);
+
+ break;
+ }
}
return next(action);
diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js
index edf0f3d..bd19225 100644
--- a/app/lib/redux/stateActions.js
+++ b/app/lib/redux/stateActions.js
@@ -220,3 +220,48 @@ export const removeAllNotifications = () =>
type : 'REMOVE_ALL_NOTIFICATIONS'
};
};
+
+export const toggleChat = () =>
+{
+ return {
+ type : 'TOGGLE_CHAT'
+ };
+};
+
+export const toggleInputDisabled = () =>
+{
+ return {
+ type : 'TOGGLE_INPUT_DISABLED'
+ };
+};
+
+export const addUserMessage = (text) =>
+{
+ return {
+ type : 'ADD_NEW_USER_MESSAGE',
+ payload : { text }
+ };
+};
+
+export const addResponseMessage = (message) =>
+{
+ return {
+ type : 'ADD_NEW_RESPONSE_MESSAGE',
+ payload : { message }
+ };
+};
+
+export const addChatHistory = (chatHistory) =>
+{
+ return {
+ type : 'ADD_CHAT_HISTORY',
+ payload : { chatHistory }
+ };
+};
+
+export const dropMessages = () =>
+{
+ return {
+ type : 'DROP_MESSAGES'
+ };
+};
diff --git a/app/lib/utils.js b/app/lib/utils.js
index ebc220d..b36c7d4 100644
--- a/app/lib/utils.js
+++ b/app/lib/utils.js
@@ -4,7 +4,7 @@ export function initialize()
{
// Media query detector stuff.
mediaQueryDetectorElem =
- document.getElementById('mediasoup-demo-app-media-query-detector');
+ document.getElementById('multiparty-meeting-media-query-detector');
return Promise.resolve();
}
diff --git a/app/package-lock.json b/app/package-lock.json
index be0864d..a202150 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -1,6 +1,6 @@
{
- "name": "mediasoup-demo-app",
- "version": "2.0.0",
+ "name": "multiparty-meeting",
+ "version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -7022,6 +7022,11 @@
"object-visit": "1.0.1"
}
},
+ "marked": {
+ "version": "0.3.17",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.17.tgz",
+ "integrity": "sha512-+AKbNsjZl6jFfLPwHhWmGTqE009wTKn3RTmn9K8oUKHrX/abPJjtcRtXpYB/FFrwPJRUA86LX/de3T0knkPCmQ=="
+ },
"matchdep": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
diff --git a/app/package.json b/app/package.json
index e0a5791..e6ce555 100644
--- a/app/package.json
+++ b/app/package.json
@@ -13,6 +13,7 @@
"domready": "^1.0.8",
"hark": "^1.1.6",
"js-cookie": "^2.2.0",
+ "marked": "^0.3.17",
"mediasoup-client": "^2.0.14",
"node-random-name": "^1.0.1",
"prop-types": "^15.6.0",
diff --git a/app/stylus/components/Chat.styl b/app/stylus/components/Chat.styl
new file mode 100644
index 0000000..2836c4b
--- /dev/null
+++ b/app/stylus/components/Chat.styl
@@ -0,0 +1,124 @@
+[data-component='ChatWidget'] {
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ margin: 0 10px 10px 0;
+ max-width: 300px;
+ position: fixed;
+ right: 0;
+ width: 90vw;
+ z-index: 9999;
+
+ > .launcher {
+ align-self: flex-end;
+ margin-top: 10px;
+ background-position: center;
+ background-size: 70%;
+ background-repeat: no-repeat;
+ background-color: rgba(#000, 0.5);
+ background-image: url('/resources/images/chat-icon.svg');
+ cursor: pointer;
+ transition-property: opacity, background-color;
+ transition-duration: 0.15s;
+ border-radius: 100%;
+ height: 45px;
+ width: 45px;
+
+ &.focus {
+ outline: none;
+ }
+
+ &.on {
+ background-color: rgba(#fff, 0.7);
+ }
+
+ &.disabled {
+ pointer-events: none;
+ opacity: 0.5;
+ }
+ }
+}
+
+[data-component='Conversation'] {
+ border-radius: 5px;
+ box-shadow: 0px 2px 10px 1px #000;
+}
+
+[data-component='MessageList'] {
+ background-color: rgba(#fff, 0.9);
+ height: 50vh;
+ max-height: 350px;
+ overflow-y: scroll;
+ padding-top: 5px;
+ border-radius: 5px 5px 0px 0px;
+
+ > .message {
+ margin: 5px;
+ display: flex;
+ word-wrap: break-word;
+ color: rgba(#000, 1.0)
+
+ > .client {
+ background-color: rgba(#fff, 0.9);
+ border-radius: 5px;
+ padding: 6px;
+ max-width: 215px;
+ text-align: left;
+ margin-left: auto;
+
+ > .message-text {
+ font-size: 1.3vmin;
+ color: rgba(#000, 1.0);
+ }
+
+ > .message-time {
+ font-size: 1vmin;
+ color: rgba(#777, 1.0)
+ }
+ }
+
+ > .response {
+ background-color: rgba(#fff, 0.9);
+ border-radius: 5px;
+ padding: 6px;
+ max-width: 215px;
+ text-align: left;
+ font-size: 1.3vmin;
+
+ > .message-text {
+ font-size: 1.3vmin;
+ color: rgba(#000, 1.0);
+ }
+
+ > .message-time {
+ font-size: 1vmin;
+ color: rgba(#777, 1.0);
+ }
+ }
+ }
+}
+
+[data-component='Sender'] {
+ align-items: center;
+ display: flex;
+ background-color: rgba(#fff, 0.9);
+ height: 35px;
+ padding: 5px;
+ border-radius: 0 0 5px 5px;
+
+ > .new-message {
+ width: 100%;
+ border: 0;
+ border-radius: 5px;
+ background-color: rgba(#fff, 0.9);
+ color: #000;
+ height: 30px;
+ padding-left: 10px;
+ font-size: 1.4vmin;
+
+ &.focus {
+ outline: none;
+ }
+ }
+}
+
diff --git a/app/stylus/index.styl b/app/stylus/index.styl
index b39337a..c45c284 100644
--- a/app/stylus/index.styl
+++ b/app/stylus/index.styl
@@ -30,7 +30,7 @@ body {
height: 100%;
}
-#mediasoup-demo-app-container {
+#multiparty-meeting {
height: 100%;
width: 100%;
@@ -41,10 +41,11 @@ body {
@import './components/Peer';
@import './components/PeerView';
@import './components/Notifications';
+ @import './components/Chat';
}
// Hack to detect in JS the current media query
-#mediasoup-demo-app-media-query-detector {
+#multiparty-meeting-media-query-detector {
position: relative;
z-index: -1000;
bottom: 0;
diff --git a/server/lib/Room.js b/server/lib/Room.js
index 16085fc..a2bad3f 100644
--- a/server/lib/Room.js
+++ b/server/lib/Room.js
@@ -26,6 +26,8 @@ class Room extends EventEmitter
// Closed flag.
this._closed = false;
+ this._chatHistory = [];
+
try
{
// Protoo Room instance.
@@ -226,6 +228,38 @@ class Room extends EventEmitter
break;
}
+ case 'chat-message':
+ {
+ accept();
+
+ const { chatMessage } = request.data;
+
+ this._chatHistory.push(chatMessage);
+
+ // Spread to others via protoo.
+ this._protooRoom.spread(
+ 'chat-message-receive',
+ {
+ peerName : protooPeer.id,
+ chatMessage : chatMessage
+ },
+ [ protooPeer ]);
+
+ break;
+ }
+
+ case 'chat-history':
+ {
+ accept();
+
+ protooPeer.send(
+ 'chat-history-receive',
+ { chatHistory : this._chatHistory }
+ );
+
+ break;
+ }
+
default:
{
logger.error('unknown request.method "%s"', request.method);