feat(chat): convert to use React
- Change "features/chat" to support listening for new chat messages and storing them, removing that logic from conference.js. - Combine chat.scss and side_toolbar_container.css, and remove unused scss files. Chat is the only side panel so the two concepts have been merged. - Remove direct access to the chat feature from non-react and non-redux flows. - Modify the i18n translate function to take in an options object. By default the option "wait" is set to true, but that causes components to mount after the parent has been notified of an update, which means autoscrolling down to the latest rendered messages does not work. With "wait" set to false, the children will mount and then the parent will trigger componentDidUpdate. - Create react components for chat. Chat is the side panel plus the entiren chat feature. ChatInput is a child of Chat and is used for composing messages. ChatMessage displays one message and extends PureComponent to limit re-renders. - Fix a bug where the toolbar was not showing automatically when chat is closed and a new message is received. - Import react-transition-group to time the animation of the side panel showing/hiding and unmounting the Chat component. This gets around the issue of having to control autofocus if the component were always mounted and visibility toggled, but introduces not being able to store previous scroll state (without additional work or re-work).
This commit is contained in:
parent
8adc8a090a
commit
b7b43e8d9c
|
@ -97,6 +97,7 @@ import {
|
|||
getLocationContextRoot,
|
||||
getJitsiMeetGlobalNS
|
||||
} from './react/features/base/util';
|
||||
import { addMessage } from './react/features/chat';
|
||||
import { showDesktopPicker } from './react/features/desktop-picker';
|
||||
import { appendSuffix } from './react/features/display-name';
|
||||
import {
|
||||
|
@ -424,10 +425,16 @@ class ConferenceConnector {
|
|||
switch (err) {
|
||||
case JitsiConferenceErrors.CHAT_ERROR:
|
||||
logger.error('Chat error.', err);
|
||||
if (isButtonEnabled('chat')) {
|
||||
if (isButtonEnabled('chat') && !interfaceConfig.filmStripOnly) {
|
||||
const [ code, msg ] = params;
|
||||
|
||||
APP.UI.showChatError(code, msg);
|
||||
APP.store.dispatch(addMessage({
|
||||
hasRead: true,
|
||||
error: code,
|
||||
message: msg,
|
||||
timestamp: Date.now(),
|
||||
type: 'error'
|
||||
}));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -1819,35 +1826,6 @@ export default {
|
|||
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
|
||||
id => APP.store.dispatch(dominantSpeakerChanged(id, room)));
|
||||
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
if (isButtonEnabled('chat')) {
|
||||
room.on(
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
(id, body, ts) => {
|
||||
let nick = getDisplayName(id);
|
||||
|
||||
if (!nick) {
|
||||
nick = `${
|
||||
interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME} (${
|
||||
id})`;
|
||||
}
|
||||
|
||||
APP.API.notifyReceivedChatMessage({
|
||||
id,
|
||||
nick,
|
||||
body,
|
||||
ts
|
||||
});
|
||||
APP.UI.addMessage(id, nick, body, ts);
|
||||
}
|
||||
);
|
||||
APP.UI.addListener(UIEvents.MESSAGE_CREATED, message => {
|
||||
APP.API.notifySendingChatMessage(message);
|
||||
room.sendTextMessage(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
|
||||
APP.store.dispatch(localParticipantConnectionStatusChanged(
|
||||
JitsiParticipantConnectionStatus.INTERRUPTED));
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
/**
|
||||
* Project animations
|
||||
**/
|
||||
|
||||
/**
|
||||
* Slide in animation for extended toolbar (inner) panel.
|
||||
*/
|
||||
|
||||
// FIX: Can't use percentage because of breaking animation when width is changed
|
||||
// (100% of 0 is also zero) Extracted this to config variable.
|
||||
@include keyframes(slideInExt) {
|
||||
from { left: -$sidebarWidth; }
|
||||
to { left: 0; }
|
||||
}
|
||||
|
||||
@include keyframes(slideOutExt) {
|
||||
from { left: 0; }
|
||||
to { left: -$sidebarWidth; }
|
||||
}
|
206
css/_chat.scss
206
css/_chat.scss
|
@ -1,21 +1,58 @@
|
|||
#sideToolbarContainer {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
display: flex;
|
||||
/**
|
||||
* Make the sidebar flush with the top of the toolbar. Take the size of
|
||||
* the toolbar and subtract from 100%.
|
||||
*/
|
||||
height: calc(100% - #{$newToolbarSizeWithPadding});
|
||||
left: -$sidebarWidth;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: left 0.5s;
|
||||
width: $sidebarWidth;
|
||||
z-index: $sideToolbarContainerZ;
|
||||
|
||||
/**
|
||||
* The sidebar (chat) is off-screen when hidden. Move it flush to the left
|
||||
* side of the window when it should be visible.
|
||||
*/
|
||||
&.slideInExt {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sideToolbarContainer__inner {
|
||||
box-sizing: border-box;
|
||||
color: #FFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: $sidebarWidth;
|
||||
}
|
||||
}
|
||||
|
||||
#chat_container * {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
#chatconversation {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
top: 15px;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
font-size: 10pt;
|
||||
line-height: 20px;
|
||||
margin-top: 15px;
|
||||
overflow: auto;
|
||||
padding: 5px;
|
||||
text-align: left;
|
||||
line-height: 20px;
|
||||
font-size: 10pt;
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
overflow: auto;
|
||||
width: $sidebarWidth;
|
||||
word-wrap: break-word;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: rgb(184, 184, 184);
|
||||
}
|
||||
|
@ -55,41 +92,52 @@
|
|||
}
|
||||
}
|
||||
|
||||
#chat_container.is-conversation-mode #chatconversation {
|
||||
visibility: visible;
|
||||
.chat-close {
|
||||
background: gray;
|
||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 100%;
|
||||
color: white;
|
||||
cursor:pointer;
|
||||
height: 10px;
|
||||
line-height: 10px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
text-align: center;
|
||||
top: 5px;
|
||||
width: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.localuser {
|
||||
color: #4C9AFF
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: red;
|
||||
#chat-input {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.remoteuser {
|
||||
color: #B8C7E0;
|
||||
}
|
||||
|
||||
.usrmsg-form {
|
||||
flex: 1;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#usermsg {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
visibility:hidden;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
width: 83%;
|
||||
height: 30px;
|
||||
border: 0px none;
|
||||
border-radius:0;
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
font-size: 10pt;
|
||||
line-height: 30px;
|
||||
padding: 5px 5px 5px 0px;
|
||||
max-height:150px;
|
||||
min-height:35px;
|
||||
border: 0px none;
|
||||
color: white;
|
||||
box-shadow: none;
|
||||
border-radius:0;
|
||||
font-size: 10pt;
|
||||
line-height: 30px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#usermsg:hover {
|
||||
|
@ -97,10 +145,6 @@
|
|||
box-shadow: none;
|
||||
}
|
||||
|
||||
#chat_container.is-conversation-mode #usermsg {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#nickname {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
|
@ -112,20 +156,7 @@
|
|||
width: 95%;
|
||||
}
|
||||
|
||||
#chat_container.is-conversation-mode #nickname {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#nickinput {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
background: #3a3a3a;
|
||||
box-shadow: inset 0 0 3px 2px #a7a7a7;
|
||||
border: 1px solid #a7a7a7;
|
||||
color: #a7a7a7;
|
||||
}
|
||||
|
||||
#chat_container .username {
|
||||
#chat_container .display-name {
|
||||
float: left;
|
||||
padding-left: 5px;
|
||||
font-weight: bold;
|
||||
|
@ -141,29 +172,45 @@
|
|||
font-size: 11px;
|
||||
}
|
||||
|
||||
#chat_container .usermessage {
|
||||
.usermessage {
|
||||
padding-top: 20px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.chatArrow {
|
||||
position: absolute;
|
||||
height: 15px;
|
||||
left: 5px;
|
||||
left: -10px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.chatmessage {
|
||||
background-color: $newToolbarBackgroundColor;;
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
width: 93%;
|
||||
margin-left: 9px;
|
||||
margin-right: auto;
|
||||
border-radius: 5px;
|
||||
border-top-left-radius: 0px;
|
||||
margin-top: 3px;
|
||||
left: 5px;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
padding-bottom: 3px;
|
||||
position: relative;
|
||||
|
||||
&.localuser .display-name {
|
||||
color: #4C9AFF
|
||||
}
|
||||
|
||||
&.error {
|
||||
.chatArrow,
|
||||
.timestamp,
|
||||
.display-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.usermessage {
|
||||
color: red;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.smiley {
|
||||
|
@ -171,11 +218,9 @@
|
|||
}
|
||||
|
||||
#smileys {
|
||||
position: absolute;
|
||||
bottom: 7px;
|
||||
right: 5px;
|
||||
background: white;
|
||||
border-radius: 50px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
height: 26px;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
|
@ -187,33 +232,40 @@
|
|||
}
|
||||
|
||||
#smileysarea {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 17%;
|
||||
min-width: 31px;
|
||||
height: 40px;
|
||||
padding: 0px;
|
||||
max-height:150px;
|
||||
min-height:35px;
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
border: 0px none;
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
display: flex;
|
||||
height: 70px;
|
||||
max-height: 150px;
|
||||
min-height: 35px;
|
||||
min-width: 31px;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
width: 17%;
|
||||
}
|
||||
|
||||
#chat_container.is-conversation-mode #smileysarea {
|
||||
visibility: visible;
|
||||
.smiley-input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#smileysContainer {
|
||||
display: none;
|
||||
.smileys-panel {
|
||||
bottom: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
border-bottom: 1px solid;
|
||||
border-top: 1px solid;
|
||||
width: 100%;
|
||||
bottom: 10%;
|
||||
transition: height 0.3s;
|
||||
width: $sidebarWidth;
|
||||
|
||||
&.show-smileys {
|
||||
height: 202px;
|
||||
}
|
||||
|
||||
#smileysContainer {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
border-bottom: 1px solid;
|
||||
border-top: 1px solid;
|
||||
}
|
||||
}
|
||||
|
||||
#smileysContainer .smiley {
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
/**
|
||||
* Toolbar side panel main container element.
|
||||
*/
|
||||
#sideToolbarContainer {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
/**
|
||||
* Make the sidebar flush with the top of the toolbar. Take the size of
|
||||
* the toolbar and subtract from 100%.
|
||||
*/
|
||||
height: calc(100% - #{$newToolbarSizeWithPadding});
|
||||
left: 0;
|
||||
max-width: $sidebarWidth;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 0;
|
||||
z-index: $sideToolbarContainerZ;
|
||||
|
||||
/**
|
||||
* Labels inside the side panel.
|
||||
*/
|
||||
label {
|
||||
color: $baseLight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form elements and blocks.
|
||||
*/
|
||||
input,
|
||||
a,
|
||||
.sideToolbarBlock,
|
||||
.form-control {
|
||||
display: block;
|
||||
margin-top: 15px;
|
||||
margin-left: 10%;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify styling of elements inside a block.
|
||||
*/
|
||||
.sideToolbarBlock {
|
||||
input, a {
|
||||
margin-left: 0;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner container, for example settings or profile.
|
||||
*/
|
||||
.sideToolbarContainer__inner {
|
||||
display: none;
|
||||
height: 100%;
|
||||
width: $sidebarWidth;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
color: #FFF;
|
||||
|
||||
.input-control {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Titles and subtitles of inner containers.
|
||||
*/
|
||||
div.title {
|
||||
margin: 24px 0 11px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main title size.
|
||||
*/
|
||||
div.title {
|
||||
color: $toolbarTitleColor;
|
||||
text-align: center;
|
||||
font-size: $toolbarTitleFontSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* First element after a title.
|
||||
*/
|
||||
.first {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.side-toolbar-close {
|
||||
background: gray;
|
||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 100%;
|
||||
color: white;
|
||||
cursor:pointer;
|
||||
height: 10px;
|
||||
line-height: 10px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
text-align: center;
|
||||
top: 5px;
|
||||
width: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
|
@ -303,27 +303,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* START of slide in animation for extended toolbar panel.
|
||||
*/
|
||||
@include keyframes(slideInExt) {
|
||||
from { width: 0px; }
|
||||
to { width: $sidebarWidth; } // TO FIX: Make this value a percentage.
|
||||
}
|
||||
|
||||
.slideInExt {
|
||||
@include animation("slideInExt .5s forwards");
|
||||
}
|
||||
|
||||
@include keyframes(slideOutExt) {
|
||||
from { width: $sidebarWidth; } // TO FIX: Make this value a percentage.
|
||||
to { width: 0px; }
|
||||
}
|
||||
|
||||
.slideOutExt {
|
||||
@include animation("slideOutExt .5s forwards");
|
||||
}
|
||||
|
||||
/**
|
||||
* START of fade in animation for main toolbar
|
||||
*/
|
||||
|
|
|
@ -16,10 +16,6 @@
|
|||
|
||||
/* Mixins END */
|
||||
|
||||
/* Animations BEGIN */
|
||||
|
||||
@import "animations";
|
||||
|
||||
/* Animations END */
|
||||
|
||||
/* Fonts BEGIN */
|
||||
|
@ -56,7 +52,6 @@
|
|||
@import 'welcome_page';
|
||||
@import 'welcome_page_content';
|
||||
@import 'toolbars';
|
||||
@import 'side_toolbar_container';
|
||||
@import 'jquery.contextMenu';
|
||||
@import 'keyboard-shortcuts';
|
||||
@import 'redirect_page';
|
||||
|
|
|
@ -158,6 +158,7 @@
|
|||
"title": "Enter a nickname in the box below",
|
||||
"popover": "Choose a nickname"
|
||||
},
|
||||
"error": "Error: your message \"__originalText__\" was not sent. Reason: __error__",
|
||||
"messagebox": "Enter text..."
|
||||
},
|
||||
"settings": {
|
||||
|
|
|
@ -4,9 +4,6 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
|
|||
|
||||
const UI = {};
|
||||
|
||||
import Chat from './side_pannels/chat/Chat';
|
||||
import SidePanels from './side_pannels/SidePanels';
|
||||
import SideContainerToggler from './side_pannels/SideContainerToggler';
|
||||
import messageHandler from './util/MessageHandler';
|
||||
import UIUtil from './util/UIUtil';
|
||||
import UIEvents from '../../service/UI/UIEvents';
|
||||
|
@ -22,6 +19,7 @@ import {
|
|||
showParticipantJoinedNotification
|
||||
} from '../../react/features/base/participants';
|
||||
import { destroyLocalTracks } from '../../react/features/base/tracks';
|
||||
import { toggleChat } from '../../react/features/chat';
|
||||
import { openDisplayNamePrompt } from '../../react/features/display-name';
|
||||
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
|
||||
import { setFilmstripVisible } from '../../react/features/filmstrip';
|
||||
|
@ -89,9 +87,6 @@ const UIListeners = new Map([
|
|||
], [
|
||||
UIEvents.SHARED_VIDEO_CLICKED,
|
||||
() => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
|
||||
], [
|
||||
UIEvents.TOGGLE_CHAT,
|
||||
() => UI.toggleChat()
|
||||
], [
|
||||
UIEvents.TOGGLE_FILMSTRIP,
|
||||
() => UI.toggleFilmstrip()
|
||||
|
@ -175,17 +170,6 @@ UI.notifyConferenceDestroyed = function(reason) {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Show chat error.
|
||||
* @param err the Error
|
||||
* @param msg
|
||||
*/
|
||||
UI.showChatError = function(err, msg) {
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
Chat.chatAddError(err, msg);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Change nickname for the user.
|
||||
* @param {string} id user id
|
||||
|
@ -193,10 +177,6 @@ UI.showChatError = function(err, msg) {
|
|||
*/
|
||||
UI.changeDisplayName = function(id, displayName) {
|
||||
VideoLayout.onDisplayNameChanged(id, displayName);
|
||||
|
||||
if (APP.conference.isLocalId(id) || id === 'localVideoContainer') {
|
||||
Chat.setChatConversationMode(Boolean(displayName));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -276,7 +256,6 @@ UI.start = function() {
|
|||
// Set the defaults for prompt dialogs.
|
||||
$.prompt.setDefaults({ persistent: false });
|
||||
|
||||
SideContainerToggler.init(eventEmitter);
|
||||
Filmstrip.init(eventEmitter);
|
||||
|
||||
VideoLayout.init(eventEmitter);
|
||||
|
@ -295,21 +274,15 @@ UI.start = function() {
|
|||
if (interfaceConfig.filmStripOnly) {
|
||||
$('body').addClass('filmstrip-only');
|
||||
APP.store.dispatch(setNotificationsEnabled(false));
|
||||
} else {
|
||||
// Initialize recording mode UI.
|
||||
if (config.iAmRecorder) {
|
||||
// in case of iAmSipGateway keep local video visible
|
||||
if (!config.iAmSipGateway) {
|
||||
VideoLayout.setLocalVideoVisible(false);
|
||||
}
|
||||
|
||||
APP.store.dispatch(setToolboxEnabled(false));
|
||||
APP.store.dispatch(setNotificationsEnabled(false));
|
||||
UI.messageHandler.enablePopups(false);
|
||||
} else if (config.iAmRecorder) {
|
||||
// in case of iAmSipGateway keep local video visible
|
||||
if (!config.iAmSipGateway) {
|
||||
VideoLayout.setLocalVideoVisible(false);
|
||||
}
|
||||
|
||||
// Initialize side panels
|
||||
SidePanels.init(eventEmitter);
|
||||
APP.store.dispatch(setToolboxEnabled(false));
|
||||
APP.store.dispatch(setNotificationsEnabled(false));
|
||||
UI.messageHandler.enablePopups(false);
|
||||
}
|
||||
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
|
@ -335,7 +308,6 @@ UI.bindEvents = () => {
|
|||
*
|
||||
*/
|
||||
function onResize() {
|
||||
SideContainerToggler.resize();
|
||||
VideoLayout.resizeVideoArea();
|
||||
}
|
||||
|
||||
|
@ -495,11 +467,6 @@ UI.updateUserStatus = (user, status) => {
|
|||
{ status: UIUtil.escapeHtml(status) });
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles smileys in the chat.
|
||||
*/
|
||||
UI.toggleSmileys = () => Chat.toggleSmileys();
|
||||
|
||||
/**
|
||||
* Toggles filmstrip.
|
||||
*/
|
||||
|
@ -516,22 +483,9 @@ UI.toggleFilmstrip = function() {
|
|||
UI.isFilmstripVisible = () => Filmstrip.isFilmstripVisible();
|
||||
|
||||
/**
|
||||
* @returns {true} if the chat panel is currently visible, and false otherwise.
|
||||
* Toggles the visibility of the chat panel.
|
||||
*/
|
||||
UI.isChatVisible = () => Chat.isVisible();
|
||||
|
||||
/**
|
||||
* Toggles chat panel.
|
||||
*/
|
||||
UI.toggleChat = () => UI.toggleSidePanel('chat_container');
|
||||
|
||||
/**
|
||||
* Toggles the given side panel.
|
||||
*
|
||||
* @param {String} sidePanelId the identifier of the side panel to toggle
|
||||
*/
|
||||
UI.toggleSidePanel = sidePanelId => SideContainerToggler.toggle(sidePanelId);
|
||||
|
||||
UI.toggleChat = () => APP.store.dispatch(toggleChat());
|
||||
|
||||
/**
|
||||
* Handle new user display name.
|
||||
|
@ -741,17 +695,6 @@ UI.hideStats = function() {
|
|||
VideoLayout.hideStats();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add chat message.
|
||||
* @param {string} from user id
|
||||
* @param {string} displayName user nickname
|
||||
* @param {string} message message text
|
||||
* @param {number} stamp timestamp when message was created
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
UI.addMessage = function(from, displayName, message, stamp) {
|
||||
Chat.updateChatConversation(from, displayName, message, stamp);
|
||||
};
|
||||
|
||||
UI.notifyTokenAuthFailed = function() {
|
||||
messageHandler.showError({
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
/* global $, APP */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { setVisiblePanel } from '../../../react/features/side-panel';
|
||||
|
||||
/**
|
||||
* Handles open and close of the extended toolbar side panel
|
||||
* (chat, settings, etc.).
|
||||
*
|
||||
* @type {{init, toggle, isVisible, hide, show, resize}}
|
||||
*/
|
||||
const SideContainerToggler = {
|
||||
/**
|
||||
* Initialises this toggler by registering the listeners.
|
||||
*
|
||||
* @param eventEmitter
|
||||
*/
|
||||
init(eventEmitter) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
|
||||
// We may not have a side toolbar container, for example, in
|
||||
// filmstrip-only mode.
|
||||
const sideToolbarContainer
|
||||
= document.getElementById('sideToolbarContainer');
|
||||
|
||||
if (!sideToolbarContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adds a listener for the animationend event that would take care of
|
||||
// hiding all internal containers when the extendedToolbarPanel is
|
||||
// closed.
|
||||
sideToolbarContainer.addEventListener(
|
||||
'animationend',
|
||||
e => {
|
||||
if (e.animationName === 'slideOutExt') {
|
||||
$('#sideToolbarContainer').children()
|
||||
.each(function() {
|
||||
/* eslint-disable no-invalid-this */
|
||||
if ($(this).hasClass('show')) {
|
||||
SideContainerToggler.hideInnerContainer($(this));
|
||||
}
|
||||
/* eslint-enable no-invalid-this */
|
||||
});
|
||||
}
|
||||
},
|
||||
false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the container with the given element id.
|
||||
*
|
||||
* @param {String} elementId the identifier of the container element to
|
||||
* toggle
|
||||
*/
|
||||
toggle(elementId) {
|
||||
const elementSelector = $(`#${elementId}`);
|
||||
const isSelectorVisible = elementSelector.hasClass('show');
|
||||
|
||||
if (isSelectorVisible) {
|
||||
this.hide();
|
||||
APP.store.dispatch(setVisiblePanel(null));
|
||||
} else {
|
||||
if (this.isVisible()) {
|
||||
$('#sideToolbarContainer').children()
|
||||
.each(function() {
|
||||
/* eslint-disable no-invalid-this */
|
||||
if ($(this).id !== elementId && $(this).hasClass('show')) {
|
||||
SideContainerToggler.hideInnerContainer($(this));
|
||||
}
|
||||
/* eslint-enable no-invalid-this */
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isVisible()) {
|
||||
this.show();
|
||||
}
|
||||
|
||||
this.showInnerContainer(elementSelector);
|
||||
APP.store.dispatch(setVisiblePanel(elementId));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns {true} if the side toolbar panel is currently visible,
|
||||
* otherwise returns {false}.
|
||||
*/
|
||||
isVisible() {
|
||||
return $('#sideToolbarContainer').hasClass('slideInExt');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns {true} if the side toolbar panel is currently hovered and
|
||||
* {false} otherwise.
|
||||
*/
|
||||
isHovered() {
|
||||
return $('#sideToolbarContainer:hover').length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides the side toolbar panel with a slide out animation.
|
||||
*/
|
||||
hide() {
|
||||
$('#sideToolbarContainer')
|
||||
.removeClass('slideInExt')
|
||||
.addClass('slideOutExt');
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the side toolbar panel with a slide in animation.
|
||||
*/
|
||||
show() {
|
||||
if (!this.isVisible()) {
|
||||
$('#sideToolbarContainer')
|
||||
.removeClass('slideOutExt')
|
||||
.addClass('slideInExt');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides the inner container given by the selector.
|
||||
*
|
||||
* @param {Object} containerSelector the jquery selector for the
|
||||
* element to hide
|
||||
*/
|
||||
hideInnerContainer(containerSelector) {
|
||||
containerSelector.removeClass('show').addClass('hide');
|
||||
|
||||
this.eventEmitter.emit(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
|
||||
containerSelector.attr('id'), false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the inner container given by the selector.
|
||||
*
|
||||
* @param {Object} containerSelector the jquery selector for the
|
||||
* element to show
|
||||
*/
|
||||
showInnerContainer(containerSelector) {
|
||||
|
||||
// Before showing the container, make sure there is no other visible.
|
||||
// If we quickly show a container, while another one is animating
|
||||
// and animation never ends, so we do not really hide the first one and
|
||||
// we end up with to shown panels
|
||||
$('#sideToolbarContainer').children()
|
||||
.each(function() {
|
||||
/* eslint-disable no-invalid-this */
|
||||
if ($(this).hasClass('show')) {
|
||||
SideContainerToggler.hideInnerContainer($(this));
|
||||
}
|
||||
/* eslint-enable no-invalid-this */
|
||||
});
|
||||
|
||||
containerSelector.removeClass('hide').addClass('show');
|
||||
|
||||
this.eventEmitter.emit(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
|
||||
containerSelector.attr('id'), true);
|
||||
},
|
||||
|
||||
/**
|
||||
* TO FIX: do we need to resize the chat?
|
||||
*/
|
||||
resize() {
|
||||
// let [width, height] = UIUtil.getSidePanelSize();
|
||||
// Chat.resizeChat(width, height);
|
||||
}
|
||||
};
|
||||
|
||||
export default SideContainerToggler;
|
|
@ -1,13 +0,0 @@
|
|||
import Chat from './chat/Chat';
|
||||
import { isButtonEnabled } from '../../../react/features/toolbox';
|
||||
|
||||
const SidePanels = {
|
||||
init(eventEmitter) {
|
||||
// Initialize chat
|
||||
if (isButtonEnabled('chat')) {
|
||||
Chat.init(eventEmitter);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default SidePanels;
|
|
@ -1,404 +0,0 @@
|
|||
/* global APP, $ */
|
||||
|
||||
import { processReplacements } from './Replacement';
|
||||
import VideoLayout from '../../videolayout/VideoLayout';
|
||||
|
||||
import UIUtil from '../../util/UIUtil';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
import { smileys } from './smileys';
|
||||
|
||||
import { addMessage, markAllRead } from '../../../../react/features/chat';
|
||||
import {
|
||||
dockToolbox,
|
||||
getToolboxHeight
|
||||
} from '../../../../react/features/toolbox';
|
||||
|
||||
let unreadMessages = 0;
|
||||
const sidePanelsContainerId = 'sideToolbarContainer';
|
||||
const htmlStr = `
|
||||
<div id="chat_container" class="sideToolbarContainer__inner">
|
||||
<div id="nickname">
|
||||
<span data-i18n="chat.nickname.title"></span>
|
||||
<form>
|
||||
<input type='text'
|
||||
class="input-control" id="nickinput" autofocus
|
||||
data-i18n="[placeholder]chat.nickname.popover">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="chatconversation"></div>
|
||||
<textarea id="usermsg" autofocus
|
||||
data-i18n="[placeholder]chat.messagebox"></textarea>
|
||||
<div id="smileysarea">
|
||||
<div id="smileys">
|
||||
<img src="images/smile.svg"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function initHTML() {
|
||||
$(`#${sidePanelsContainerId}`)
|
||||
.append(htmlStr);
|
||||
|
||||
// make sure we translate the panel, as adding it can be after i18n
|
||||
// library had initialized and translated already present html
|
||||
APP.translation.translateElement($(`#${sidePanelsContainerId}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* The container id, which is and the element id.
|
||||
*/
|
||||
const CHAT_CONTAINER_ID = 'chat_container';
|
||||
|
||||
/**
|
||||
* Updates visual notification, indicating that a message has arrived.
|
||||
*/
|
||||
function updateVisualNotification() {
|
||||
// XXX The rewrite of the toolbar in React delayed the availability of the
|
||||
// element unreadMessages. In order to work around the delay, I introduced
|
||||
// and utilized unreadMsgSelector in addition to unreadMsgElement.
|
||||
const unreadMsgSelector = $('#unreadMessages');
|
||||
const unreadMsgElement
|
||||
= unreadMsgSelector.length > 0 ? unreadMsgSelector[0] : undefined;
|
||||
|
||||
if (unreadMessages && unreadMsgElement) {
|
||||
unreadMsgElement.innerHTML = unreadMessages.toString();
|
||||
|
||||
APP.store.dispatch(dockToolbox(true));
|
||||
|
||||
const chatButtonElement
|
||||
= document.getElementById('toolbar_button_chat');
|
||||
const leftIndent
|
||||
= (UIUtil.getTextWidth(chatButtonElement)
|
||||
- UIUtil.getTextWidth(unreadMsgElement)) / 2;
|
||||
const topIndent
|
||||
= ((UIUtil.getTextHeight(chatButtonElement)
|
||||
- UIUtil.getTextHeight(unreadMsgElement)) / 2) - 5;
|
||||
|
||||
unreadMsgElement.setAttribute(
|
||||
'style',
|
||||
`top:${topIndent}; left:${leftIndent};`);
|
||||
} else {
|
||||
unreadMsgSelector.html('');
|
||||
}
|
||||
|
||||
if (unreadMsgElement) {
|
||||
unreadMsgSelector.parent()[unreadMessages > 0 ? 'show' : 'hide']();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the current time in the format it is shown to the user
|
||||
* @returns {string}
|
||||
*/
|
||||
function getCurrentTime(stamp) {
|
||||
const now = stamp ? new Date(stamp) : new Date();
|
||||
let hour = now.getHours();
|
||||
let minute = now.getMinutes();
|
||||
let second = now.getSeconds();
|
||||
|
||||
if (hour.toString().length === 1) {
|
||||
hour = `0${hour}`;
|
||||
}
|
||||
if (minute.toString().length === 1) {
|
||||
minute = `0${minute}`;
|
||||
}
|
||||
if (second.toString().length === 1) {
|
||||
second = `0${second}`;
|
||||
}
|
||||
|
||||
return `${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleSmileys() {
|
||||
const smileys = $('#smileysContainer'); // eslint-disable-line no-shadow
|
||||
|
||||
smileys.slideToggle();
|
||||
|
||||
$('#usermsg').focus();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function addClickFunction(smiley, number) {
|
||||
smiley.onclick = function addSmileyToMessage() {
|
||||
const usermsg = $('#usermsg');
|
||||
let message = usermsg.val();
|
||||
|
||||
message += smileys[`smiley${number}`];
|
||||
usermsg.val(message);
|
||||
usermsg.get(0).setSelectionRange(message.length, message.length);
|
||||
toggleSmileys();
|
||||
usermsg.focus();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the smileys container to the chat
|
||||
*/
|
||||
function addSmileys() {
|
||||
const smileysContainer = document.createElement('div');
|
||||
|
||||
smileysContainer.id = 'smileysContainer';
|
||||
for (let i = 1; i <= 21; i++) {
|
||||
const smileyContainer = document.createElement('div');
|
||||
|
||||
smileyContainer.id = `smiley${i}`;
|
||||
smileyContainer.className = 'smileyContainer';
|
||||
const smiley = document.createElement('img');
|
||||
|
||||
smiley.src = `images/smileys/smiley${i}.svg`;
|
||||
smiley.className = 'smiley';
|
||||
addClickFunction(smiley, i);
|
||||
smileyContainer.appendChild(smiley);
|
||||
smileysContainer.appendChild(smileyContainer);
|
||||
}
|
||||
|
||||
$('#chat_container').append(smileysContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the chat conversation.
|
||||
*/
|
||||
function resizeChatConversation() {
|
||||
// FIXME: this function can all be done with CSS. If Chat is ever rewritten,
|
||||
// do not copy over this logic.
|
||||
const msgareaHeight = $('#usermsg').outerHeight();
|
||||
const chatspace = $(`#${CHAT_CONTAINER_ID}`);
|
||||
const width = chatspace.width();
|
||||
const chat = $('#chatconversation');
|
||||
const smileys = $('#smileysarea'); // eslint-disable-line no-shadow
|
||||
|
||||
smileys.height(msgareaHeight);
|
||||
$('#smileys').css('bottom', (msgareaHeight - 26) / 2);
|
||||
$('#smileysContainer').css('bottom', msgareaHeight);
|
||||
chat.width(width - 10);
|
||||
|
||||
const maybeAMagicNumberForPaddingAndMargin = 100;
|
||||
const offset = maybeAMagicNumberForPaddingAndMargin
|
||||
+ msgareaHeight + getToolboxHeight();
|
||||
|
||||
chat.height(window.innerHeight - offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus input after 400 ms
|
||||
* Found input by id
|
||||
*
|
||||
* @param id {string} input id
|
||||
*/
|
||||
function deferredFocus(id) {
|
||||
setTimeout(() => $(`#${id}`).focus(), 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat related user interface.
|
||||
*/
|
||||
const Chat = {
|
||||
/**
|
||||
* Initializes chat related interface.
|
||||
*/
|
||||
init(eventEmitter) {
|
||||
initHTML();
|
||||
if (APP.conference.getLocalDisplayName()) {
|
||||
Chat.setChatConversationMode(true);
|
||||
}
|
||||
|
||||
$('#smileys').click(() => {
|
||||
Chat.toggleSmileys();
|
||||
});
|
||||
|
||||
$('#nickinput').keydown(function(event) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
const val = this.value; // eslint-disable-line no-invalid-this
|
||||
|
||||
this.value = '';// eslint-disable-line no-invalid-this
|
||||
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, val);
|
||||
deferredFocus('usermsg');
|
||||
}
|
||||
});
|
||||
|
||||
const usermsg = $('#usermsg');
|
||||
|
||||
usermsg.keydown(function(event) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
const value = this.value; // eslint-disable-line no-invalid-this
|
||||
|
||||
usermsg.val('').trigger('autosize.resize');
|
||||
this.focus();// eslint-disable-line no-invalid-this
|
||||
|
||||
const message = UIUtil.escapeHtml(value);
|
||||
|
||||
eventEmitter.emit(UIEvents.MESSAGE_CREATED, message);
|
||||
}
|
||||
});
|
||||
|
||||
const onTextAreaResize = function() {
|
||||
resizeChatConversation();
|
||||
Chat.scrollChatToBottom();
|
||||
};
|
||||
|
||||
usermsg.autosize({ callback: onTextAreaResize });
|
||||
|
||||
eventEmitter.on(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
|
||||
(containerId, isVisible) => {
|
||||
if (containerId !== CHAT_CONTAINER_ID || !isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
unreadMessages = 0;
|
||||
APP.store.dispatch(markAllRead());
|
||||
updateVisualNotification();
|
||||
|
||||
// Undock the toolbar when the chat is shown and if we're in a
|
||||
// video mode.
|
||||
if (VideoLayout.isLargeVideoVisible()) {
|
||||
APP.store.dispatch(dockToolbox(false));
|
||||
}
|
||||
|
||||
// if we are in conversation mode focus on the text input
|
||||
// if we are not, focus on the display name input
|
||||
deferredFocus(
|
||||
APP.conference.getLocalDisplayName()
|
||||
? 'usermsg'
|
||||
: 'nickinput');
|
||||
});
|
||||
|
||||
addSmileys();
|
||||
updateVisualNotification();
|
||||
},
|
||||
|
||||
/**
|
||||
* Appends the given message to the chat conversation.
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
updateChatConversation(id, displayName, message, stamp) {
|
||||
const isFromLocalParticipant = APP.conference.isLocalId(id);
|
||||
let divClassName = '';
|
||||
|
||||
if (isFromLocalParticipant) {
|
||||
divClassName = 'localuser';
|
||||
} else {
|
||||
divClassName = 'remoteuser';
|
||||
|
||||
if (!Chat.isVisible()) {
|
||||
unreadMessages++;
|
||||
updateVisualNotification();
|
||||
}
|
||||
}
|
||||
|
||||
// replace links and smileys
|
||||
// Strophe already escapes special symbols on sending,
|
||||
// so we escape here only tags to avoid double &
|
||||
const escMessage = message.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br/>');
|
||||
const escDisplayName = UIUtil.escapeHtml(displayName);
|
||||
const timestamp = getCurrentTime(stamp);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message = processReplacements(escMessage);
|
||||
|
||||
const messageContainer
|
||||
= `${'<div class="chatmessage">'
|
||||
+ '<img src="images/chatArrow.svg" class="chatArrow">'
|
||||
+ '<div class="username '}${divClassName}">${escDisplayName
|
||||
}</div><div class="timestamp">${timestamp
|
||||
}</div><div class="usermessage">${message}</div>`
|
||||
+ '</div>';
|
||||
|
||||
$('#chatconversation').append(messageContainer);
|
||||
$('#chatconversation').animate(
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
|
||||
|
||||
const markAsRead = Chat.isVisible() || isFromLocalParticipant;
|
||||
|
||||
APP.store.dispatch(addMessage(
|
||||
escDisplayName, message, timestamp, markAsRead));
|
||||
},
|
||||
|
||||
/**
|
||||
* Appends error message to the conversation
|
||||
* @param errorMessage the received error message.
|
||||
* @param originalText the original message.
|
||||
*/
|
||||
chatAddError(errorMessage, originalText) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
errorMessage = UIUtil.escapeHtml(errorMessage);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
originalText = UIUtil.escapeHtml(originalText);
|
||||
|
||||
$('#chatconversation').append(
|
||||
`${'<div class="errorMessage"><b>Error: </b>Your message'}${
|
||||
originalText ? ` "${originalText}"` : ''
|
||||
} was not sent.${
|
||||
errorMessage ? ` Reason: ${errorMessage}` : ''}</div>`);
|
||||
$('#chatconversation').animate(
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the chat conversation mode.
|
||||
* Conversation mode is the normal chat mode, non conversation mode is
|
||||
* where we ask user to input its display name.
|
||||
* @param {boolean} isConversationMode if chat should be in
|
||||
* conversation mode or not.
|
||||
*/
|
||||
setChatConversationMode(isConversationMode) {
|
||||
$(`#${CHAT_CONTAINER_ID}`)
|
||||
.toggleClass('is-conversation-mode', isConversationMode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes the chat area.
|
||||
*/
|
||||
resizeChat(width, height) {
|
||||
$(`#${CHAT_CONTAINER_ID}`).width(width)
|
||||
.height(height);
|
||||
|
||||
resizeChatConversation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates if the chat is currently visible.
|
||||
*/
|
||||
isVisible() {
|
||||
return UIUtil.isVisible(
|
||||
document.getElementById(CHAT_CONTAINER_ID));
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows and hides the window with the smileys
|
||||
*/
|
||||
toggleSmileys,
|
||||
|
||||
/**
|
||||
* Scrolls chat to the bottom.
|
||||
*/
|
||||
scrollChatToBottom() {
|
||||
setTimeout(
|
||||
() => {
|
||||
const chatconversation = $('#chatconversation');
|
||||
|
||||
// XXX Prevent TypeError: undefined is not an object when the
|
||||
// Web browser does not support WebRTC (yet).
|
||||
chatconversation.length > 0
|
||||
&& chatconversation.scrollTop(
|
||||
chatconversation[0].scrollHeight);
|
||||
},
|
||||
5);
|
||||
}
|
||||
};
|
||||
|
||||
export default Chat;
|
|
@ -55,10 +55,6 @@ const KeyboardShortcut = {
|
|||
APP.UI.clickOnVideo(num);
|
||||
}
|
||||
|
||||
// esc while the smileys are visible hides them
|
||||
} else if (key === 'ESCAPE'
|
||||
&& $('#smileysContainer').is(':visible')) {
|
||||
APP.UI.toggleSmileys();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -180,6 +180,18 @@
|
|||
"prop-types": "^15.5.10",
|
||||
"styled-components": "1.4.6 - 3"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.1.tgz",
|
||||
"integrity": "sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==",
|
||||
"requires": {
|
||||
"chain-function": "^1.0.0",
|
||||
"dom-helpers": "^3.2.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.5.6",
|
||||
"warning": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -4384,9 +4396,9 @@
|
|||
}
|
||||
},
|
||||
"chain-function": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.0.tgz",
|
||||
"integrity": "sha1-DUqzfn4Y6tC9xHuSB2QRjOWHM9w="
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz",
|
||||
"integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg=="
|
||||
},
|
||||
"chalk": {
|
||||
"version": "1.1.3",
|
||||
|
@ -12489,15 +12501,25 @@
|
|||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.1.tgz",
|
||||
"integrity": "sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.4.0.tgz",
|
||||
"integrity": "sha512-Xv5d55NkJUxUzLCImGSanK8Cl/30sgpOEMGc5m86t8+kZwrPxPCPcFqyx83kkr+5Lz5gs6djuvE5By+gce+VjA==",
|
||||
"requires": {
|
||||
"chain-function": "^1.0.0",
|
||||
"dom-helpers": "^3.2.0",
|
||||
"dom-helpers": "^3.3.1",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.5.6",
|
||||
"warning": "^3.0.0"
|
||||
"prop-types": "^15.6.2",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.3.1",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
"react-native-vector-icons": "4.4.2",
|
||||
"react-native-webrtc": "github:jitsi/react-native-webrtc#be3de15bb988cfabbb62cd4b3b06f4c920ee5ba0",
|
||||
"react-redux": "5.0.7",
|
||||
"react-transition-group": "2.4.0",
|
||||
"redux": "4.0.0",
|
||||
"redux-thunk": "2.2.0",
|
||||
"styled-components": "1.4.6",
|
||||
|
|
|
@ -5,13 +5,15 @@ import { translate as reactI18nextTranslate } from 'react-i18next';
|
|||
* Wraps a specific React Component in order to enable translations in it.
|
||||
*
|
||||
* @param {Component} component - The React Component to wrap.
|
||||
* @param {Object} options - Additional options to pass into react-i18next's
|
||||
* initialization.
|
||||
* @returns {Component} The React Component which wraps {@link component} and
|
||||
* enables translations in it.
|
||||
*/
|
||||
export function translate(component) {
|
||||
export function translate(component, options = { wait: true }) {
|
||||
// Use the default list of namespaces.
|
||||
return (
|
||||
reactI18nextTranslate([ 'main', 'languages' ], { wait: true })(
|
||||
reactI18nextTranslate([ 'main', 'languages' ], options)(
|
||||
component));
|
||||
}
|
||||
|
||||
|
|
|
@ -3,21 +3,32 @@
|
|||
*
|
||||
* {
|
||||
* type: ADD_MESSAGE,
|
||||
* displayName: string
|
||||
* hasRead: boolean,
|
||||
* id: string,
|
||||
* messageType: string,
|
||||
* message: string,
|
||||
* timestamp: string,
|
||||
* userName: string
|
||||
* }
|
||||
*/
|
||||
export const ADD_MESSAGE = Symbol('ADD_MESSAGE');
|
||||
|
||||
/**
|
||||
* The type of the action which updates which is the most recent message that
|
||||
* has been seen by the local participant.
|
||||
* The type of the action which signals a send a chat message to everyone in the
|
||||
* conference.
|
||||
*
|
||||
* {
|
||||
* type: SET_LAST_READ_MESSAGE,
|
||||
* message: Object
|
||||
* type: SEND_MESSAGE,
|
||||
* message: string
|
||||
* }
|
||||
*/
|
||||
export const SET_LAST_READ_MESSAGE = Symbol('SET_LAST_READ_MESSAGE');
|
||||
export const SEND_MESSAGE = Symbol('SEND_MESSAGE');
|
||||
|
||||
/**
|
||||
* The type of the action which signals to toggle the display of the chat panel.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_CHAT
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_CHAT = Symbol('TOGGLE_CHAT');
|
||||
|
|
|
@ -1,63 +1,61 @@
|
|||
import { ADD_MESSAGE, SET_LAST_READ_MESSAGE } from './actionTypes';
|
||||
import { ADD_MESSAGE, SEND_MESSAGE, TOGGLE_CHAT } from './actionTypes';
|
||||
|
||||
/* eslint-disable max-params */
|
||||
|
||||
/**
|
||||
* Adds a chat message to the collection of messages.
|
||||
*
|
||||
* @param {string} userName - The username to display of the participant that
|
||||
* authored the message.
|
||||
* @param {string} message - The received message to display.
|
||||
* @param {string} timestamp - A timestamp to display for when the message was
|
||||
* received.
|
||||
* @param {boolean} hasRead - Whether or not to immediately mark the message as
|
||||
* read.
|
||||
* @param {Object} messageDetails - The chat message to save.
|
||||
* @param {string} messageDetails.displayName - The displayName of the
|
||||
* participant that authored the message.
|
||||
* @param {boolean} messageDetails.hasRead - Whether or not to immediately mark
|
||||
* the message as read.
|
||||
* @param {string} messageDetails.message - The received message to display.
|
||||
* @param {string} messageDetails.messageType - The kind of message, such as
|
||||
* "error" or "local" or "remote".
|
||||
* @param {string} messageDetails.timestamp - A timestamp to display for when
|
||||
* the message was received.
|
||||
* @returns {{
|
||||
* type: ADD_MESSAGE,
|
||||
* displayName: string,
|
||||
* hasRead: boolean,
|
||||
* message: string,
|
||||
* messageType: string,
|
||||
* timestamp: string,
|
||||
* userName: string
|
||||
* }}
|
||||
*/
|
||||
export function addMessage(userName, message, timestamp, hasRead) {
|
||||
export function addMessage(messageDetails) {
|
||||
return {
|
||||
type: ADD_MESSAGE,
|
||||
hasRead,
|
||||
message,
|
||||
timestamp,
|
||||
userName
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-enable max-params */
|
||||
|
||||
/**
|
||||
* Sets the last read message cursor to the latest message.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function markAllRead() {
|
||||
return (dispatch, getState) => {
|
||||
const { messages } = getState()['features/chat'];
|
||||
|
||||
dispatch(setLastReadMessage(messages[messages.length - 1]));
|
||||
...messageDetails
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last read message cursor to be set at the passed in message. The
|
||||
* assumption is that messages will be ordered chronologically.
|
||||
* Sends a chat message to everyone in the conference.
|
||||
*
|
||||
* @param {Object} message - The message from the redux state.
|
||||
* @param {string} message - The chat message to send out.
|
||||
* @returns {{
|
||||
* type: SET_LAST_READ_MESSAGE,
|
||||
* message: Object
|
||||
* type: SEND_MESSAGE,
|
||||
* message: string
|
||||
* }}
|
||||
*/
|
||||
export function setLastReadMessage(message) {
|
||||
export function sendMessage(message) {
|
||||
return {
|
||||
type: SET_LAST_READ_MESSAGE,
|
||||
type: SEND_MESSAGE,
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles display of the chat side panel.
|
||||
*
|
||||
* @returns {{
|
||||
* type: TOGGLE_CHAT
|
||||
* }}
|
||||
*/
|
||||
export function toggleChat() {
|
||||
return {
|
||||
type: TOGGLE_CHAT
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,312 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Transition from 'react-transition-group/Transition';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { getLocalParticipant } from '../../base/participants';
|
||||
|
||||
import { toggleChat } from '../actions';
|
||||
|
||||
import ChatInput from './ChatInput';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import DisplayNameForm from './DisplayNameForm';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Chat}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The JitsiConference instance to send messages to.
|
||||
*/
|
||||
_conference: Object,
|
||||
|
||||
/**
|
||||
* Whether or not chat is displayed.
|
||||
*/
|
||||
_isOpen: Boolean,
|
||||
|
||||
/**
|
||||
* The local participant's ID.
|
||||
*/
|
||||
_localUserId: String,
|
||||
|
||||
/**
|
||||
* All the chat messages in the conference.
|
||||
*/
|
||||
_messages: Array<Object>,
|
||||
|
||||
/**
|
||||
* Whether or not to block chat access with a nickname input form.
|
||||
*/
|
||||
_showNamePrompt: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to change the chat panel status.
|
||||
*/
|
||||
dispatch: Dispatch<*>
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@Chat}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* User provided nickname when the input text is provided in the view.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
message: string
|
||||
};
|
||||
|
||||
/**
|
||||
* React Component for holding the chat feature in a side panel that slides in
|
||||
* and out of view.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class Chat extends Component<Props, State> {
|
||||
|
||||
/**
|
||||
* Reference to the HTML element used for typing in a chat message.
|
||||
*/
|
||||
_chatInput: ?HTMLElement;
|
||||
|
||||
/**
|
||||
* Whether or not the {@code Chat} component is off-screen, having finished
|
||||
* its hiding animation.
|
||||
*/
|
||||
_isExited: boolean;
|
||||
|
||||
/**
|
||||
* Reference to the HTML element at the end of the list of displayed chat
|
||||
* messages. Used for scrolling to the end of the chat messages.
|
||||
*/
|
||||
_messagesListEnd: ?HTMLElement;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code Chat} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._chatInput = null;
|
||||
this._isExited = true;
|
||||
this._messagesListEnd = null;
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onCloseClick = this._onCloseClick.bind(this);
|
||||
this._renderMessage = this._renderMessage.bind(this);
|
||||
this._renderPanelContent = this._renderPanelContent.bind(this);
|
||||
this._setChatInputRef = this._setChatInputRef.bind(this);
|
||||
this._setMessageListEndRef = this._setMessageListEndRef.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._scrollMessagesToBottom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates chat input focus.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props._messages !== prevProps._messages) {
|
||||
this._scrollMessagesToBottom();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Transition
|
||||
in = { this.props._isOpen }
|
||||
timeout = { 500 }>
|
||||
{ this._renderPanelContent }
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
_onCloseClick: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked to hide {@code Chat}.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCloseClick() {
|
||||
this.props.dispatch(toggleChat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a React Element for showing chat messages and a form to send new
|
||||
* chat messages.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderChat() {
|
||||
const messages = this.props._messages.map(this._renderMessage);
|
||||
|
||||
messages.push(<div
|
||||
key = 'end-marker'
|
||||
ref = { this._setMessageListEndRef } />);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'sideToolbarContainer__inner'
|
||||
id = 'chat_container'>
|
||||
<div id = 'chatconversation'>
|
||||
{ messages }
|
||||
</div>
|
||||
<ChatInput getChatInputRef = { this._setChatInputRef } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderMessage: (Object) => void;
|
||||
|
||||
/**
|
||||
* Called by {@code _onSubmitMessage} to create the chat div.
|
||||
*
|
||||
* @param {string} message - The chat message to display.
|
||||
* @param {string} id - The chat message ID to use as a unique key.
|
||||
* @returns {Array<ReactElement>}
|
||||
*/
|
||||
_renderMessage(message: Object, id: string) {
|
||||
return (
|
||||
<ChatMessage
|
||||
key = { id }
|
||||
message = { message } />
|
||||
);
|
||||
}
|
||||
|
||||
_renderPanelContent: (string) => React$Node | null;
|
||||
|
||||
/**
|
||||
* Renders the contents of the chat panel, depending on the current
|
||||
* animation state provided by {@code Transition}.
|
||||
*
|
||||
* @param {string} state - The current display transition state of the
|
||||
* {@code Chat} component, as provided by {@code Transition}.
|
||||
* @private
|
||||
* @returns {ReactElement | null}
|
||||
*/
|
||||
_renderPanelContent(state) {
|
||||
this._isExited = state === 'exited';
|
||||
|
||||
const { _isOpen, _showNamePrompt } = this.props;
|
||||
const ComponentToRender = !_isOpen && state === 'exited'
|
||||
? null
|
||||
: (
|
||||
<div>
|
||||
<div
|
||||
className = 'chat-close'
|
||||
onClick = { this._onCloseClick }>X</div>
|
||||
{ _showNamePrompt
|
||||
? <DisplayNameForm /> : this._renderChat() }
|
||||
</div>
|
||||
);
|
||||
let className = '';
|
||||
|
||||
if (_isOpen) {
|
||||
className = 'slideInExt';
|
||||
} else if (this._isExited) {
|
||||
className = 'invisible';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { className }
|
||||
id = 'sideToolbarContainer'>
|
||||
{ ComponentToRender }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically scrolls the displayed chat messages down to the latest.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_scrollMessagesToBottom() {
|
||||
if (this._messagesListEnd) {
|
||||
this._messagesListEnd.scrollIntoView({
|
||||
behavior: this._isExited ? 'auto' : 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_setChatInputRef: (?HTMLElement) => void;
|
||||
|
||||
/**
|
||||
* Sets a reference to the HTML text input element used for typing in chat
|
||||
* messages.
|
||||
*
|
||||
* @param {Object} chatInput - The input for typing chat messages.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setChatInputRef(chatInput: ?HTMLElement) {
|
||||
this._chatInput = chatInput;
|
||||
}
|
||||
|
||||
_setMessageListEndRef: (?HTMLElement) => void;
|
||||
|
||||
/**
|
||||
* Sets a reference to the HTML element at the bottom of the message list.
|
||||
*
|
||||
* @param {Object} messageListEnd - The HTML element.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setMessageListEndRef(messageListEnd: ?HTMLElement) {
|
||||
this._messagesListEnd = messageListEnd;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to {@link Chat} React {@code Component}
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The redux store/state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: Object,
|
||||
* _isOpen: boolean,
|
||||
* _messages: Array<Object>,
|
||||
* _showNamePrompt: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { isOpen, messages } = state['features/chat'];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference,
|
||||
_isOpen: isOpen,
|
||||
_messages: messages,
|
||||
_showNamePrompt: !localParticipant.name
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(Chat));
|
|
@ -1,13 +1,20 @@
|
|||
import PropTypes from 'prop-types';
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getUnreadCount } from '../functions';
|
||||
|
||||
/**
|
||||
* FIXME: Move this UI logic to a generic component that can be used for
|
||||
* {@code ParticipantCounter} as well.
|
||||
* The type of the React {@code Component} props of {@link ChatCounter}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The value of to display as a count.
|
||||
*/
|
||||
_count: number
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a count of the number of
|
||||
|
@ -15,13 +22,7 @@ import { getUnreadCount } from '../functions';
|
|||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class ChatCounter extends Component {
|
||||
static propTypes = {
|
||||
/**
|
||||
* The number of unread chat messages in the conference.
|
||||
*/
|
||||
_count: PropTypes.number
|
||||
};
|
||||
class ChatCounter extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { sendMessage } from '../actions';
|
||||
|
||||
import SmileysPanel from './SmileysPanel';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ChatInput}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Invoked to send chat messages.
|
||||
*/
|
||||
dispatch: Dispatch<*>,
|
||||
|
||||
/**
|
||||
* Optional callback to get a reference to the chat input element.
|
||||
*/
|
||||
getChatInputRef?: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link ChatInput}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* User provided nickname when the input text is provided in the view.
|
||||
*/
|
||||
message: string,
|
||||
|
||||
/**
|
||||
* Whether or not the smiley selector is visible.
|
||||
*/
|
||||
showSmileysPanel: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React Component for drafting and submitting a chat message.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class ChatInput extends Component<Props, State> {
|
||||
_textArea: ?HTMLTextAreaElement;
|
||||
|
||||
state = {
|
||||
message: '',
|
||||
showSmileysPanel: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ChatInput} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._textArea = null;
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onDetectSubmit = this._onDetectSubmit.bind(this);
|
||||
this._onMessageChange = this._onMessageChange.bind(this);
|
||||
this._onSmileySelect = this._onSmileySelect.bind(this);
|
||||
this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
|
||||
this._setTextAreaRef = this._setTextAreaRef.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
/**
|
||||
* HTML Textareas do not support autofocus. Simulate autofocus by
|
||||
* manually focusing.
|
||||
*/
|
||||
this.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const smileysPanelClassName = `${this.state.showSmileysPanel
|
||||
? 'show-smileys' : 'hide-smileys'} smileys-panel`;
|
||||
|
||||
return (
|
||||
<div id = 'chat-input' >
|
||||
<div className = 'smiley-input'>
|
||||
<div id = 'smileysarea'>
|
||||
<div id = 'smileys'>
|
||||
<img
|
||||
onClick = { this._onToggleSmileysPanel }
|
||||
src = '../../../../images/smile.svg' />
|
||||
</div>
|
||||
</div>
|
||||
<div className = { smileysPanelClassName }>
|
||||
<SmileysPanel
|
||||
onSmileySelect = { this._onSmileySelect } />
|
||||
</div>
|
||||
</div>
|
||||
<div className = 'usrmsg-form'>
|
||||
<textarea
|
||||
data-i18n = '[placeholder]chat.messagebox'
|
||||
id = 'usermsg'
|
||||
onChange = { this._onMessageChange }
|
||||
onKeyDown = { this._onDetectSubmit }
|
||||
placeholder = { 'Enter Text...' }
|
||||
ref = { this._setTextAreaRef }
|
||||
value = { this.state.message } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes cursor focus on this component's text area.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
blur() {
|
||||
this._textArea && this._textArea.blur();
|
||||
}
|
||||
|
||||
/**
|
||||
* Place cursor focus on this component's text area.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
focus() {
|
||||
this._textArea && this._textArea.focus();
|
||||
}
|
||||
|
||||
_onDetectSubmit: (Object) => void;
|
||||
|
||||
/**
|
||||
* Detects if enter has been pressed. If so, submit the message in the chat
|
||||
* window.
|
||||
*
|
||||
* @param {string} event - Keyboard event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDetectSubmit(event) {
|
||||
if (event.keyCode === 13
|
||||
&& event.shiftKey === false) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatch(sendMessage(this.state.message));
|
||||
|
||||
this.setState({ message: '' });
|
||||
}
|
||||
}
|
||||
|
||||
_onMessageChange: (Object) => void;
|
||||
|
||||
/**
|
||||
* Updates the known message the user is drafting.
|
||||
*
|
||||
* @param {string} event - Keyboard event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMessageChange(event) {
|
||||
this.setState({ message: event.target.value });
|
||||
}
|
||||
|
||||
_onSmileySelect: (string) => void;
|
||||
|
||||
/**
|
||||
* Appends a selected smileys to the chat message draft.
|
||||
*
|
||||
* @param {string} smileyText - The value of the smiley to append to the
|
||||
* chat message.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSmileySelect(smileyText) {
|
||||
this.setState({
|
||||
message: `${this.state.message} ${smileyText}`,
|
||||
showSmileysPanel: false
|
||||
});
|
||||
|
||||
this.focus();
|
||||
}
|
||||
|
||||
_onToggleSmileysPanel: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked to hide or show the smileys selector.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleSmileysPanel() {
|
||||
this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
|
||||
|
||||
this.focus();
|
||||
}
|
||||
|
||||
_setTextAreaRef: (?HTMLTextAreaElement) => void;
|
||||
|
||||
/**
|
||||
* Sets the reference to the HTML TextArea.
|
||||
*
|
||||
* @param {HTMLAudioElement} textAreaElement - The HTML text area element.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setTextAreaRef(textAreaElement: ?HTMLTextAreaElement) {
|
||||
this._textArea = textAreaElement;
|
||||
|
||||
if (this.props.getChatInputRef) {
|
||||
this.props.getChatInputRef(textAreaElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(ChatInput);
|
|
@ -0,0 +1,114 @@
|
|||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
import { processReplacements } from '../replacement';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Chat}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The redux representation of a chat message.
|
||||
*/
|
||||
message: Object,
|
||||
|
||||
/**
|
||||
* Invoked to receive translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays as passed in chat message.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class ChatMessage extends PureComponent<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { message } = this.props;
|
||||
let messageTypeClassname = '';
|
||||
let messagetoDisplay = message.message;
|
||||
|
||||
switch (message.messageType) {
|
||||
case 'local':
|
||||
messageTypeClassname = 'localuser';
|
||||
|
||||
break;
|
||||
case 'error':
|
||||
messageTypeClassname = 'error';
|
||||
messagetoDisplay = this.props.t('chat.error', {
|
||||
error: message.error,
|
||||
originalText: messagetoDisplay
|
||||
});
|
||||
break;
|
||||
default:
|
||||
messageTypeClassname = 'remoteuser';
|
||||
}
|
||||
|
||||
// replace links and smileys
|
||||
// Strophe already escapes special symbols on sending,
|
||||
// so we escape here only tags to avoid double &
|
||||
const escMessage = messagetoDisplay.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br/>');
|
||||
const messageWithHTML = processReplacements(escMessage);
|
||||
|
||||
return (
|
||||
<div className = { `chatmessage ${messageTypeClassname}` }>
|
||||
<img
|
||||
className = 'chatArrow'
|
||||
src = '../../../../images/chatArrow.svg' />
|
||||
<div className = 'display-name'>
|
||||
{ message.displayName }
|
||||
</div>
|
||||
<div className = { 'timestamp' }>
|
||||
{ ChatMessage.formatTimestamp(message.timestamp) }
|
||||
</div>
|
||||
<div
|
||||
className = 'usermessage'
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML = {{ __html: messageWithHTML }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a timestamp formatted for display.
|
||||
*
|
||||
* @param {number} timestamp - The timestamp for the chat message.
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
static formatTimestamp(timestamp) {
|
||||
const now = new Date(timestamp);
|
||||
let hour = now.getHours();
|
||||
let minute = now.getMinutes();
|
||||
let second = now.getSeconds();
|
||||
|
||||
if (hour.toString().length === 1) {
|
||||
hour = `0${hour}`;
|
||||
}
|
||||
|
||||
if (minute.toString().length === 1) {
|
||||
minute = `0${minute}`;
|
||||
}
|
||||
|
||||
if (second.toString().length === 1) {
|
||||
second = `0${second}`;
|
||||
}
|
||||
|
||||
return `${hour}:${minute}:${second}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(ChatMessage, { wait: false });
|
|
@ -0,0 +1,142 @@
|
|||
// @flow
|
||||
|
||||
import { FieldTextStateless } from '@atlaskit/field-text';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
participantDisplayNameChanged
|
||||
} from '../../base/participants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@DisplayNameForm}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The ID of the local participant.
|
||||
*/
|
||||
_localParticipantId: string,
|
||||
|
||||
/**
|
||||
* Invoked to set the local participant display name.
|
||||
*/
|
||||
dispatch: Dispatch<*>,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@DisplayNameForm}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* User provided display name when the input text is provided in the view.
|
||||
*/
|
||||
displayName: string
|
||||
};
|
||||
|
||||
/**
|
||||
* React Component for requesting the local participant to set a display name.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class DisplayNameForm extends Component<Props, State> {
|
||||
state = {
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code DisplayNameForm} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onDisplayNameChange = this._onDisplayNameChange.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div id = 'nickname'>
|
||||
<span>{ this.props.t('chat.nickname.title') }</span>
|
||||
<form onSubmit = { this._onSubmit }>
|
||||
<FieldTextStateless
|
||||
autoFocus = { true }
|
||||
id = 'nickinput'
|
||||
onChange = { this._onDisplayNameChange }
|
||||
placeholder = { t('chat.nickname.popover') }
|
||||
type = 'text'
|
||||
value = { this.state.displayName } />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_onDisplayNameChange: (Object) => void;
|
||||
|
||||
/**
|
||||
* Dispatches an action update the entered display name.
|
||||
*
|
||||
* @param {event} event - Keyboard event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDisplayNameChange(event: Object) {
|
||||
this.setState({ displayName: event.target.value });
|
||||
}
|
||||
|
||||
_onSubmit: (Object) => void;
|
||||
|
||||
/**
|
||||
* Dispatches an action to hit enter to change your display name.
|
||||
*
|
||||
* @param {event} event - Keyboard event
|
||||
* that will check if user has pushed the enter key.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit(event: Object) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatch(participantDisplayNameChanged(
|
||||
this.props._localParticipantId,
|
||||
this.state.displayName));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code DisplayNameForm} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _localParticipantId: string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_localParticipantId: getLocalParticipant(state).id
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(DisplayNameForm));
|
|
@ -0,0 +1,69 @@
|
|||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { smileys } from '../smileys';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link SmileysPanel}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Callback to invoke when a smiley is selected. The smiley will be passed
|
||||
* back.
|
||||
*/
|
||||
onSmileySelect: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React Component showing smileys that can be be shown in chat.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class SmileysPanel extends PureComponent<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const smileyItems = Object.keys(smileys).map(smileyKey => {
|
||||
const onSelectFunction = this._getOnSmileySelectCallback(smileyKey);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'smileyContainer'
|
||||
id = { smileyKey }
|
||||
key = { smileyKey }>
|
||||
<img
|
||||
className = 'smiley'
|
||||
id = { smileyKey }
|
||||
onClick = { onSelectFunction }
|
||||
src = { `images/smileys/${smileyKey}.svg` } />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div id = 'smileysContainer'>
|
||||
{ smileyItems }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to bind a smiley's click handler.
|
||||
*
|
||||
* @param {string} smileyKey - The key from the {@link smileys} object
|
||||
* that should be added to the chat message.
|
||||
* @private
|
||||
* @returns {Function}
|
||||
*/
|
||||
_getOnSmileySelectCallback(smileyKey) {
|
||||
return () => this.props.onSmileySelect(smileys[smileyKey]);
|
||||
}
|
||||
}
|
||||
|
||||
export default SmileysPanel;
|
|
@ -1 +1,2 @@
|
|||
export Chat from './Chat';
|
||||
export ChatCounter from './ChatCounter';
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
// @flow
|
||||
|
||||
import UIUtil from '../../../modules/UI/util/UIUtil';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
||||
import { CONFERENCE_JOINED } from '../base/conference';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getParticipantById } from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||
import { isButtonEnabled, showToolbox } from '../toolbox';
|
||||
|
||||
import { SEND_MESSAGE } from './actionTypes';
|
||||
import { addMessage } from './actions';
|
||||
import { INCOMING_MSG_SOUND_ID } from './constants';
|
||||
import { INCOMING_MSG_SOUND_FILE } from './sounds';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig : Object;
|
||||
|
||||
/**
|
||||
* Implements the middleware of the chat feature.
|
||||
|
@ -38,6 +45,19 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
typeof APP === 'undefined'
|
||||
|| _addChatMsgListener(action.conference, store);
|
||||
break;
|
||||
|
||||
case SEND_MESSAGE:
|
||||
if (typeof APP !== 'undefined') {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
if (conference) {
|
||||
const escapedMessage = UIUtil.escapeHtml(action.message);
|
||||
|
||||
APP.API.notifySendingChatMessage(escapedMessage);
|
||||
conference.sendTextMessage(escapedMessage);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
@ -49,19 +69,53 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
*
|
||||
* @param {JitsiConference} conference - The conference instance on which the
|
||||
* new event listener will be registered.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified action to the specified store.
|
||||
* @param {Object} store - The redux store object.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _addChatMsgListener(conference, { dispatch }) {
|
||||
// XXX Currently, there's no need to remove the listener, because the
|
||||
// JitsiConference instance cannot be reused. Hence, the listener will be
|
||||
// gone with the JitsiConference instance.
|
||||
function _addChatMsgListener(conference, { dispatch, getState }) {
|
||||
if ((typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly)
|
||||
|| !isButtonEnabled('chat')) {
|
||||
return;
|
||||
}
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
() => {
|
||||
APP.UI.isChatVisible()
|
||||
|| dispatch(playSound(INCOMING_MSG_SOUND_ID));
|
||||
});
|
||||
(id, message, timestamp) => {
|
||||
const state = getState();
|
||||
const { isOpen: isChatOpen } = state['features/chat'];
|
||||
|
||||
if (!isChatOpen) {
|
||||
dispatch(playSound(INCOMING_MSG_SOUND_ID));
|
||||
dispatch(showToolbox(4000));
|
||||
}
|
||||
|
||||
// Provide a default for for the case when a message is being
|
||||
// backfilled for a participant that has left the conference.
|
||||
const participant = getParticipantById(state, id) || {};
|
||||
const displayName = participant.name
|
||||
|| `${interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME} (${id})`;
|
||||
const hasRead = participant.local || isChatOpen;
|
||||
|
||||
APP.API.notifyReceivedChatMessage({
|
||||
body: message,
|
||||
id,
|
||||
nick: displayName,
|
||||
ts: timestamp
|
||||
});
|
||||
|
||||
const timestampToDate = timestamp
|
||||
? new Date(timestamp) : new Date();
|
||||
const millisecondsTimestamp = timestampToDate.getTime();
|
||||
|
||||
dispatch(addMessage({
|
||||
displayName,
|
||||
hasRead,
|
||||
id,
|
||||
messageType: participant.local ? 'local' : 'remote',
|
||||
message,
|
||||
timestamp: millisecondsTimestamp
|
||||
}));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,24 +2,24 @@
|
|||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
SET_LAST_READ_MESSAGE
|
||||
} from './actionTypes';
|
||||
import { ADD_MESSAGE, TOGGLE_CHAT } from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
open: false,
|
||||
messages: [],
|
||||
lastReadMessage: null
|
||||
isOpen: false,
|
||||
lastReadMessage: undefined,
|
||||
messages: []
|
||||
};
|
||||
|
||||
ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case ADD_MESSAGE: {
|
||||
const newMessage = {
|
||||
displayName: action.displayName,
|
||||
error: action.error,
|
||||
id: action.id,
|
||||
messageType: action.messageType,
|
||||
message: action.message,
|
||||
timestamp: action.timestamp,
|
||||
userName: action.userName
|
||||
timestamp: action.timestamp
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -33,10 +33,11 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
|||
};
|
||||
}
|
||||
|
||||
case SET_LAST_READ_MESSAGE:
|
||||
case TOGGLE_CHAT:
|
||||
return {
|
||||
...state,
|
||||
lastReadMessage: action.message
|
||||
isOpen: !state.isOpen,
|
||||
lastReadMessage: state.messages[state.messages.length - 1]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
import { regexes } from './smileys';
|
||||
|
||||
/**
|
||||
* Processes links and smileys in "body"
|
||||
* Processes links and smileys in "body".
|
||||
*
|
||||
* @param {string} body - The message body.
|
||||
* @returns {string} Message body with image tags and href tags.
|
||||
*/
|
||||
export function processReplacements(body) {
|
||||
// make links clickable + add smileys
|
||||
return smilify(linkify(body));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds and replaces all links in the links in "body"
|
||||
* with their <a href=""></a>
|
||||
* Finds and replaces all links in the links in "body" with an href tag.
|
||||
*
|
||||
* @param {string} inputText - The message body.
|
||||
* @returns {string} The text replaced with HTML tags for links.
|
||||
*/
|
||||
export function linkify(inputText) {
|
||||
function linkify(inputText) {
|
||||
let replacedText;
|
||||
|
||||
/* eslint-disable no-useless-escape, max-len */
|
||||
|
@ -38,7 +44,10 @@ export function linkify(inputText) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Replaces common smiley strings with images
|
||||
* Replaces common smiley strings with images.
|
||||
*
|
||||
* @param {string} body - The message body.
|
||||
* @returns {string} Body returned with smiley replaced.
|
||||
*/
|
||||
function smilify(body) {
|
||||
if (!body) {
|
|
@ -9,11 +9,11 @@ import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
|
|||
import { obtainConfig } from '../../base/config';
|
||||
import { connect, disconnect } from '../../base/connection';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { Chat } from '../../chat';
|
||||
import { Filmstrip } from '../../filmstrip';
|
||||
import { CalleeInfoContainer } from '../../invite';
|
||||
import { LargeVideo } from '../../large-video';
|
||||
import { NotificationsContainer } from '../../notifications';
|
||||
import { SidePanel } from '../../side-panel';
|
||||
import {
|
||||
LAYOUTS,
|
||||
getCurrentLayout,
|
||||
|
@ -223,7 +223,7 @@ class Conference extends Component<Props> {
|
|||
</div>
|
||||
|
||||
{ filmstripOnly || <Toolbox /> }
|
||||
{ filmstripOnly || <SidePanel /> }
|
||||
{ filmstripOnly || <Chat /> }
|
||||
|
||||
<NotificationsContainer />
|
||||
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
/**
|
||||
* The type of the action which signals to close the side panel.
|
||||
*
|
||||
* {
|
||||
* type: CLOSE_PANEL,
|
||||
* }
|
||||
*/
|
||||
export const CLOSE_PANEL = Symbol('CLOSE_PANEL');
|
||||
|
||||
/**
|
||||
* The type of the action which to set the name of the current panel being
|
||||
* displayed in the side panel.
|
||||
*
|
||||
* {
|
||||
* type: SET_VISIBLE_PANEL,
|
||||
* current: string|null
|
||||
* }
|
||||
*/
|
||||
export const SET_VISIBLE_PANEL = Symbol('SET_VISIBLE_PANEL');
|
||||
|
||||
/**
|
||||
* The type of the action which signals to toggle the display of chat in the
|
||||
* side panel.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_CHAT
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_CHAT = Symbol('TOGGLE_CHAT');
|
|
@ -1,49 +0,0 @@
|
|||
import {
|
||||
CLOSE_PANEL,
|
||||
SET_VISIBLE_PANEL,
|
||||
TOGGLE_CHAT
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Dispatches an action to close the currently displayed side panel.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function closePanel() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: CLOSE_PANEL,
|
||||
current: getState()['features/side-panel'].current
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the redux store with the currently displayed side panel.
|
||||
*
|
||||
* @param {string|null} name - The name of the side panel being displayed. Null
|
||||
* (or falsy) should be set if no side panel is being displayed.
|
||||
* @returns {{
|
||||
* type: SET_VISIBLE_PANEL,
|
||||
* current: string
|
||||
* }}
|
||||
*/
|
||||
export function setVisiblePanel(name = null) {
|
||||
return {
|
||||
type: SET_VISIBLE_PANEL,
|
||||
current: name
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles display of the chat side panel.
|
||||
*
|
||||
* @returns {{
|
||||
* type: TOGGLE_CHAT
|
||||
* }}
|
||||
*/
|
||||
export function toggleChat() {
|
||||
return {
|
||||
type: TOGGLE_CHAT
|
||||
};
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { closePanel } from '../actions';
|
||||
|
||||
/**
|
||||
* React Component for holding features in a side panel that slides in and out.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class SidePanel extends Component {
|
||||
/**
|
||||
* {@code SidePanel} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code SidePanel} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onCloseClick = this._onCloseClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<div id = 'sideToolbarContainer'>
|
||||
<div
|
||||
className = 'side-toolbar-close'
|
||||
onClick = { this._onCloseClick }>
|
||||
X
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to hide {@code SidePanel}.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCloseClick() {
|
||||
this.props.dispatch(closePanel());
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(SidePanel);
|
|
@ -1 +0,0 @@
|
|||
export { default as SidePanel } from './SidePanel';
|
|
@ -1,6 +0,0 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
|
@ -1,32 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import { CLOSE_PANEL, TOGGLE_CHAT } from './actionTypes';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Middleware that catches actions related to the non-reactified web side panel.
|
||||
*
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
if (typeof APP !== 'object') {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case CLOSE_PANEL:
|
||||
APP.UI.toggleSidePanel(action.current);
|
||||
break;
|
||||
|
||||
case TOGGLE_CHAT:
|
||||
APP.UI.toggleChat();
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
|
@ -1,18 +0,0 @@
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_VISIBLE_PANEL } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/side-panel.
|
||||
*/
|
||||
ReducerRegistry.register('features/side-panel', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case SET_VISIBLE_PANEL:
|
||||
return {
|
||||
...state,
|
||||
current: action.current
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
|
@ -1,7 +1,5 @@
|
|||
/* @flow */
|
||||
|
||||
import SideContainerToggler
|
||||
from '../../../modules/UI/side_pannels/SideContainerToggler';
|
||||
|
||||
import {
|
||||
clearToolboxTimeout,
|
||||
|
@ -90,7 +88,7 @@ export function hideToolbox(force: boolean = false): Function {
|
|||
if (!force
|
||||
&& (hovered
|
||||
|| state['features/invite'].calleeInfoVisible
|
||||
|| SideContainerToggler.isVisible())) {
|
||||
|| state['features/chat'].isOpen)) {
|
||||
dispatch(
|
||||
setToolboxTimeout(
|
||||
() => dispatch(hideToolbox()),
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
participantUpdated
|
||||
} from '../../../base/participants';
|
||||
import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
|
||||
import { ChatCounter } from '../../../chat';
|
||||
import { ChatCounter, toggleChat } from '../../../chat';
|
||||
import { toggleDocument } from '../../../etherpad';
|
||||
import { openFeedbackDialog } from '../../../feedback';
|
||||
import {
|
||||
|
@ -41,7 +41,6 @@ import {
|
|||
openSettingsDialog
|
||||
} from '../../../settings';
|
||||
import { toggleSharedVideo } from '../../../shared-video';
|
||||
import { toggleChat } from '../../../side-panel';
|
||||
import { SpeakerStats } from '../../../speaker-stats';
|
||||
import { TileViewButton } from '../../../video-layout';
|
||||
import {
|
||||
|
@ -1032,7 +1031,6 @@ function _mapStateToProps(state) {
|
|||
iAmRecorder
|
||||
} = state['features/base/config'];
|
||||
const sharedVideoStatus = state['features/shared-video'].status;
|
||||
const { current } = state['features/side-panel'];
|
||||
const {
|
||||
alwaysVisible,
|
||||
fullScreen,
|
||||
|
@ -1066,7 +1064,7 @@ function _mapStateToProps(state) {
|
|||
}
|
||||
|
||||
return {
|
||||
_chatOpen: current === 'chat_container',
|
||||
_chatOpen: state['features/chat'].isOpen,
|
||||
_conference: conference,
|
||||
_desktopSharingEnabled: desktopSharingEnabled,
|
||||
_desktopSharingDisabledTooltipKey: desktopSharingDisabledTooltipKey,
|
||||
|
|
|
@ -2,11 +2,6 @@ export default {
|
|||
NICKNAME_CHANGED: 'UI.nickname_changed',
|
||||
PINNED_ENDPOINT: 'UI.pinned_endpoint',
|
||||
|
||||
/**
|
||||
* Notifies that local user created text message.
|
||||
*/
|
||||
MESSAGE_CREATED: 'UI.message_created',
|
||||
|
||||
/**
|
||||
* Notifies that local user changed email.
|
||||
*/
|
||||
|
@ -34,7 +29,6 @@ export default {
|
|||
* Notifies that the audio only mode was toggled.
|
||||
*/
|
||||
TOGGLE_AUDIO_ONLY: 'UI.toggle_audioonly',
|
||||
TOGGLE_CHAT: 'UI.toggle_chat',
|
||||
|
||||
/**
|
||||
* Notifies that a command to toggle the filmstrip has been issued. The
|
||||
|
|
Loading…
Reference in New Issue