diff --git a/css/_chat-preview.scss b/css/_chat-preview.scss new file mode 100644 index 0000000000000..5dffbc15f391d --- /dev/null +++ b/css/_chat-preview.scss @@ -0,0 +1,103 @@ +.chat-preview { + position: absolute; + left: 47px; + top: auto; + bottom: 30px; + z-index: $zindex3; + height: 60vh; + overflow: hidden; + display: flex; + flex-direction: column; + + &::before { + content: ''; + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + // background: linear-gradient(180deg, rgba(0,0,0,1) 0%, rgba(255,255,255,0) 71%); + + height: 50px; + } + + #chatconversation { + display: flex; + flex-direction: column-reverse; + overflow: hidden; + } +} + +.chat-preview-group { + display: flex; + flex-direction: row; + align-items: flex-end; + margin-bottom: 30px; + transition: opacity 0.3s ease-in, transform .5s ease-in; + align-items: flex-end; + + &.expired { + opacity: 0; + transform: translateY(-200%); + } + + .chatmessage { + background-color: #FFF; + border-radius: 6px 6px 6px 0px; + display: inline-block; + margin-top: 3px; + color: #393939; + } + + .chatmessage-wrapper:not(:last-child) .chatmessage { + margin-bottom: 10px; + } + + + .display-name { + display: none; + } + + &.error { + .chatmessage { + background-color: $defaultWarningColor; + border-radius: 0px; + font-weight: 100; + } + + .display-name { + display: none; + } + } + + .chatmessage-wrapper { + max-width: 100%; + + .replywrapper { + display: flex; + flex-direction: row; + align-items: center; + + .messageactions { + align-self: stretch; + border-left: 1px solid $chatActionsSeparatorColor; + display: flex; + flex-direction: column; + justify-content: center; + padding: 5px; + + .toolbox-icon { + cursor: pointer; + } + } + } + } + .avatar { + min-width: 45px; + min-height: 45px; + width: 45px !important; + height: 45px !important; + margin-right: 10px; + border: 2px solid #EAEAEA; + } +} diff --git a/css/_welcome_page.scss b/css/_welcome_page.scss index a79ea723a0d8e..d192a768db4ac 100755 --- a/css/_welcome_page.scss +++ b/css/_welcome_page.scss @@ -1,6 +1,9 @@ body.welcome-page { background: inherit; overflow: auto; + .leftwatermark { + display: block; + } } @media only screen and (max-width: 800px) { diff --git a/css/main.scss b/css/main.scss index b2586bc387a4c..6a21aa936d360 100755 --- a/css/main.scss +++ b/css/main.scss @@ -99,5 +99,5 @@ $flagsImagePath: "../images/"; @import 'modals/invite/invite_more'; @import 'modals/security/security'; @import 'premeeting-screens'; - +@import 'chat-preview' /* Modules END */ diff --git a/package-lock.json b/package-lock.json index 4ee9a2fc01225..d84108586e5dd 100755 --- a/package-lock.json +++ b/package-lock.json @@ -2843,7 +2843,7 @@ }, "@jitsi/sdp-simulcast": { "version": "0.3.0", - "resolved": "https://npr.saal.ai/@jitsi%2fsdp-simulcast/-/sdp-simulcast-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/@jitsi/sdp-simulcast/-/sdp-simulcast-0.3.0.tgz", "integrity": "sha512-lxHfIWgTvdVY7F7BOcC3OaFvyvLsQJVRBCQvfmz4/Pk21/FdCyeBW4gv9ogfxxisjarU8gPX7/up4Z3C17wuXw==", "requires": { "sdp-transform": "2.3.0" @@ -5140,7 +5140,7 @@ }, "async": { "version": "0.9.0", - "resolved": "https://npr.saal.ai/async/-/async-0.9.0.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz", "integrity": "sha1-rDYTsdqb7RtHUQu0ZRuJMeRxRsc=" }, "async-each": { @@ -6984,7 +6984,7 @@ }, "current-executing-script": { "version": "0.1.3", - "resolved": "https://npr.saal.ai/current-executing-script/-/current-executing-script-0.1.3.tgz", + "resolved": "https://registry.npmjs.org/current-executing-script/-/current-executing-script-0.1.3.tgz", "integrity": "sha1-t5jfxYtc+LAPsEwd8KwmY5Z+LHA=" }, "currently-unhandled": { @@ -11119,7 +11119,7 @@ }, "lodash.clonedeep": { "version": "4.5.0", - "resolved": "https://npr.saal.ai/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, "lodash.flatten": { @@ -11129,7 +11129,7 @@ }, "lodash.isequal": { "version": "4.5.0", - "resolved": "https://npr.saal.ai/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, "lodash.isstring": { @@ -15887,7 +15887,7 @@ }, "rtcpeerconnection-shim": { "version": "1.2.15", - "resolved": "https://npr.saal.ai/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz", + "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz", "integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==", "requires": { "sdp": "^2.6.0" @@ -16179,12 +16179,12 @@ }, "sdp": { "version": "2.12.0", - "resolved": "https://npr.saal.ai/sdp/-/sdp-2.12.0.tgz", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==" }, "sdp-transform": { "version": "2.3.0", - "resolved": "https://npr.saal.ai/sdp-transform/-/sdp-transform-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.3.0.tgz", "integrity": "sha1-V6lXWUIEHYV3qGnXx01MOgvYiPY=" }, "seedrandom": { @@ -17395,12 +17395,12 @@ }, "strophe.js": { "version": "1.3.4", - "resolved": "https://npr.saal.ai/strophe.js/-/strophe.js-1.3.4.tgz", + "resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.3.4.tgz", "integrity": "sha512-jSLDG8jolhAwGOSgiJ7DTMSYK3wVoEJHKtpVRyEacQZ6CWA6z2WRPJpcFMjsIweq5aP9/XIvKUQqHBu/ZhvESA==" }, "strophejs-plugin-disco": { "version": "0.0.2", - "resolved": "https://npr.saal.ai/strophejs-plugin-disco/-/strophejs-plugin-disco-0.0.2.tgz", + "resolved": "https://registry.npmjs.org/strophejs-plugin-disco/-/strophejs-plugin-disco-0.0.2.tgz", "integrity": "sha512-T9pJFzn1ZUqZ/we9+OvI5pFdrjeb4IBMbEjK+ZWEZV036wEl8l8GOtF8AJ3sIqOMtdIiFLdFu99JiGWd7yapAQ==" }, "strophejs-plugin-stream-management": { @@ -17955,6 +17955,14 @@ } } }, + "toastr": { + "version": "2.1.4", + "resolved": "https://npr.saal.ai/toastr/-/toastr-2.1.4.tgz", + "integrity": "sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=", + "requires": { + "jquery": ">=1.12.0" + } + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -19833,7 +19841,7 @@ }, "webrtc-adapter": { "version": "7.5.0", - "resolved": "https://npr.saal.ai/webrtc-adapter/-/webrtc-adapter-7.5.0.tgz", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.5.0.tgz", "integrity": "sha512-cUqlw310uLLSYvO8FTNCVmGWSMlMt6vuSDkcYL1nW+RUvAILJ3jEIvAUgFQU5EFGnU+mf9/No14BFv3U+hoxBQ==", "requires": { "rtcpeerconnection-shim": "^1.2.15", diff --git a/package.json b/package.json index aa80a7b7916cd..d2776ff7576b5 100755 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "redux-thunk": "2.2.0", "rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#db96d11f175a22ef56c7db1ba9550835b716e615", "styled-components": "3.4.9", + "toastr": "^2.1.4", "util": "0.12.1", "uuid": "^3.1.0", "windows-iana": "^3.1.0", diff --git a/react/features/chat/components/AbstractChatPreview.js b/react/features/chat/components/AbstractChatPreview.js new file mode 100644 index 0000000000000..6408a397ab790 --- /dev/null +++ b/react/features/chat/components/AbstractChatPreview.js @@ -0,0 +1,108 @@ +// @flow + +import { Component } from 'react'; +import type { Dispatch } from 'redux'; + +import { getLocalParticipant } from '../../base/participants'; +import { sendMessage, toggleChat, setPrivateMessageRecipient, markAsRead, markPublicAsRead } from '../actions'; + +/** + * The type of the React {@code Component} props of {@code AbstractChatPreview}. + */ +export type Props = { + + /** + * True if the chat window should be rendered. + */ + _isOpen: boolean, + + /** + * All the chat messages in the conference. + */ + _messages: Array, + + /** + * The local participant. + */ + _localParticipant: Object, +}; + +/** + * Implements an abstract chat panel. + */ +export default class AbstractChatPreview extends Component {} + +/** + * Maps redux actions to the props of the component. + * + * @param {Function} dispatch - The redux action {@code dispatch} function. + * @returns {{ + * _onSendMessage: Function, + * _onToggleChat: Function + * }} + * @private + */ +export function _mapDispatchToProps(dispatch: Dispatch) { + return { + /** + * Toggles the chat window. + * + * @returns {Function} + */ + _onToggleChat() { + dispatch(toggleChat()); + }, + + /** + * Sends a text message. + * + * @private + * @param {string} text - The text message to be sent. + * @returns {void} + * @type {Function} + */ + _onSendMessage(text: string) { + dispatch(sendMessage(text)); + }, + + _setPrivateMessageRecipient(participant: Object) { + dispatch(setPrivateMessageRecipient(participant)); + }, + + _markAsRead(localParticipant: Object, participant: Object) { + dispatch(markAsRead(localParticipant, participant)); + }, + + _markPublicAsRead() { + dispatch(markPublicAsRead()); + } + }; +} + +/** + * Maps (parts of) the redux state to {@link Chat} React {@code Component} + * props. + * + * @param {Object} state - The redux store/state. + * @private + * @returns {{ + * _isOpen: boolean, + * _messages: Array, + * _showNamePrompt: boolean + * }} + */ +export function _mapStateToProps(state: Object) { + const { + isOpen, + messages + } = state['features/chat']; + + const _localParticipant = getLocalParticipant(state); + + + return { + _isOpen: isOpen, + _messages: messages, + _localParticipant + }; +} diff --git a/react/features/chat/components/AbstractMessageContainer.js b/react/features/chat/components/AbstractMessageContainer.js index 91843075ed57f..9b472456cfc61 100755 --- a/react/features/chat/components/AbstractMessageContainer.js +++ b/react/features/chat/components/AbstractMessageContainer.js @@ -7,7 +7,8 @@ export type Props = { /** * The messages array to render. */ - messages: Array + messages: Array, + localParticipant: ?Object } /** diff --git a/react/features/chat/components/web/ChatPreview.js b/react/features/chat/components/web/ChatPreview.js new file mode 100644 index 0000000000000..4fea70edab427 --- /dev/null +++ b/react/features/chat/components/web/ChatPreview.js @@ -0,0 +1,45 @@ +// @flow +import React from 'react'; + +import { translate } from '../../../base/i18n'; +import { connect } from '../../../base/redux'; +import AbstractChatPreview, { + type Props, + _mapDispatchToProps, + _mapStateToProps +} from '../AbstractChatPreview'; + +import ChatPreviewContainer from './ChatPreviewContainer'; + + +type State = { + +} + +/** + * Implements a React native component that renders the chat preview + */ +class ChatPreview extends AbstractChatPreview { + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + */ + render() { + + if (this.props._isOpen) { + return null; + } + + return ( +
+ +
+ ); + } +} + + +export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ChatPreview)); diff --git a/react/features/chat/components/web/ChatPreviewContainer.js b/react/features/chat/components/web/ChatPreviewContainer.js new file mode 100644 index 0000000000000..ab5ab9417b47c --- /dev/null +++ b/react/features/chat/components/web/ChatPreviewContainer.js @@ -0,0 +1,120 @@ +// @flow + +import React from 'react'; + +import AbstractMessageContainer, { type Props } + from '../AbstractMessageContainer'; + +import ChatPreviewGroup from './ChatPreviewGroup'; + +/** + * Displays all received chat messages, grouped by sender. + * + * @extends AbstractMessageContainer + */ +export default class ChatPreviewContainer extends AbstractMessageContainer { + /** + * Whether or not chat has been scrolled to the bottom of the screen. Used + * to determine if chat should be scrolled automatically to the bottom when + * the {@code ChatInput} resizes. + */ + _isScrolledToBottom: 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. + */ + _messagesListEndRef: Object; + + /** + * A React ref to the HTML element containing all {@code ChatMessageGroup} + * instances. + */ + _messageListRef: Object; + + /** + * Initializes a new {@code MessageContainer} instance. + * + * @param {Props} props - The React {@code Component} props to initialize + * the new {@code MessageContainer} instance with. + */ + constructor(props: Props) { + super(props); + + this._isScrolledToBottom = true; + + this._messageListRef = React.createRef(); + this._messagesListEndRef = React.createRef(); + + this._onChatScroll = this._onChatScroll.bind(this); + } + + /** + * Implements {@code Component#render}. + * + * @inheritdoc + */ + render() { + const messages = [ ...this.props.messages ] + .filter(msg => msg.senderId !== this.props.localParticipant?.id) + .reverse() + .map(message => ( + + )); + + return ( +
+ { messages } +
+
+ ); + } + + /** + * Scrolls to the bottom again if the instance had previously been scrolled + * to the bottom. This method is used when a resize has occurred below the + * instance and bottom scroll needs to be maintained. + * + * @returns {void} + */ + maybeUpdateBottomScroll() { + if (this._isScrolledToBottom) { + this.scrollToBottom(false); + } + } + + /** + * Automatically scrolls the displayed chat messages down to the latest. + * + * @param {boolean} withAnimation - Whether or not to show a scrolling + * animation. + * @returns {void} + */ + scrollToBottom(withAnimation: boolean) { + this._messagesListEndRef.current.scrollIntoView({ + behavior: withAnimation ? 'smooth' : 'auto', + block: 'nearest' + }); + } + + _onChatScroll: () => void; + + /** + * Callback invoked to listen to the current scroll location. + * + * @private + * @returns {void} + */ + _onChatScroll() { + console.log('assfadfasdfsfsdf'); + const element = this._messageListRef.current; + + this._isScrolledToBottom + = element.scrollHeight - element.scrollTop === element.clientHeight; + } +} diff --git a/react/features/chat/components/web/ChatPreviewGroup.js b/react/features/chat/components/web/ChatPreviewGroup.js new file mode 100755 index 0000000000000..70793aa53efea --- /dev/null +++ b/react/features/chat/components/web/ChatPreviewGroup.js @@ -0,0 +1,74 @@ +// @flow + +import truncate from 'lodash/truncate'; +import React, { Component } from 'react'; + +import { Avatar } from '../../../base/avatar'; + +import ChatMessage from './ChatMessage'; +import Expire from './Expire'; + +type Props = { + + /** + * Additional CSS classes to apply to the root element. + */ + className: string, + + /** + * The message to display . + */ + message: Object, +}; + +/** + * Displays a list of chat messages. Will show only the display name for the + * first chat message and the timestamp for the last chat message. + * + * @extends React.Component + */ +class ChatPreviewGroup extends Component { + static defaultProps = { + className: '' + }; + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + */ + render() { + const { className, message } = this.props; + + + if (!message) { + return null; + } + + const { senderId } = message; + + + return ( + + +
+ +
+ +
+
+ +
+ ); + } +} + +export default ChatPreviewGroup; diff --git a/react/features/chat/components/web/Expire.js b/react/features/chat/components/web/Expire.js new file mode 100644 index 0000000000000..470f087bfe026 --- /dev/null +++ b/react/features/chat/components/web/Expire.js @@ -0,0 +1,65 @@ +// @flow +import React from 'react'; + +type Props = { + children: Object, + timer: number +} + +type State = { + visible: boolean +} + +/** + * Renders a expireable element. + */ +export default class Expire extends React.Component { + state = { + visible: true + } + + timer = null + + /** + * ComponentDidMount. + * + * @inheritdoc + * @returns {undefined} + */ + componentDidMount() { + this.timer = setTimeout(() => { + this.setState({ visible: false }); + }, this.props.timer); + } + + /** + * ComponentWillUnmount. + * + * @inheritdoc + * @returns {undefined} + */ + componentWillUnmount() { + clearTimeout(this.timer); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + return ( + <> + {this.state.visible ? this.props.children + : React.Children.only( + React.cloneElement(this.props.children, { + className: `${this.props.children.props.className} expired` + }) + ) + } + + ); + } +} + diff --git a/react/features/chat/components/web/index.js b/react/features/chat/components/web/index.js index 18dd324100d02..d018ec2dff7af 100755 --- a/react/features/chat/components/web/index.js +++ b/react/features/chat/components/web/index.js @@ -1,5 +1,6 @@ // @flow export { default as Chat } from './Chat'; +export { default as ChatPreview } from './ChatPreview'; export { default as ChatCounter } from './ChatCounter'; export { default as ChatPrivacyDialog } from './ChatPrivacyDialog'; diff --git a/react/features/chat/middleware.js b/react/features/chat/middleware.js index 6c5abd9b0010f..6bdecb4766f1f 100755 --- a/react/features/chat/middleware.js +++ b/react/features/chat/middleware.js @@ -1,4 +1,5 @@ // @flow +import toast from 'toastr'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app'; import { @@ -20,7 +21,7 @@ import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; import { playSound, registerSound, unregisterSound } from '../base/sounds'; import { isButtonEnabled, showToolbox } from '../toolbox'; -import { SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes'; +import { ADD_MESSAGE, SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes'; import { addMessage, clearMessages, toggleChat } from './actions'; import { ChatPrivacyDialog } from './components'; import { @@ -65,7 +66,6 @@ MiddlewareRegistry.register(store => next => action => { case CONFERENCE_JOINED: _addChatMsgListener(action.conference, store); break; - case SEND_MESSAGE: { const state = store.getState(); const { conference } = state['features/base/conference']; diff --git a/react/features/conference/components/web/Conference.js b/react/features/conference/components/web/Conference.js index d886a7ba6a2bc..9393d2dfe30ff 100755 --- a/react/features/conference/components/web/Conference.js +++ b/react/features/conference/components/web/Conference.js @@ -8,7 +8,7 @@ import { getConferenceNameForTitle } from '../../../base/conference'; import { connect, disconnect } from '../../../base/connection'; import { translate } from '../../../base/i18n'; import { connect as reactReduxConnect } from '../../../base/redux'; -import { Chat } from '../../../chat'; +import { ChatPreview, Chat } from '../../../chat'; import { Filmstrip } from '../../../filmstrip'; import { CalleeInfoContainer } from '../../../invite'; import { LargeVideo } from '../../../large-video'; @@ -211,6 +211,10 @@ class Conference extends AbstractConference { + + { !filmstripOnly && _showPrejoin /* || _interimPrejoin*/ && } + + { !filmstripOnly && _showPrejoin /* || _interimPrejoin*/ && }
diff --git a/react/features/welcome/components/WelcomePage.web.js b/react/features/welcome/components/WelcomePage.web.js index 26687a7968d4d..7b86845e2f699 100755 --- a/react/features/welcome/components/WelcomePage.web.js +++ b/react/features/welcome/components/WelcomePage.web.js @@ -9,10 +9,11 @@ import { connect } from '../../base/redux'; import { CalendarList } from '../../calendar-sync'; import { RecentList } from '../../recent-list'; import { SettingsButton, SETTINGS_TABS } from '../../settings'; -import Background from './background'; + import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; import Tabs from './Tabs'; +import Background from './background'; /** * The pattern used to validate room name. @@ -154,16 +155,15 @@ class WelcomePage extends AbstractWelcomePage { } _onRoomNameChanged(e) { - this._onRoomChange(e); - if(e.target.value.trim() != "") { + this._onRoomChange(e); + if (e.target.value.trim() != '') { this.setState({ formDisabled: false - }) - } - else { + }); + } else { this.setState({ formDisabled: true - }) + }); } } @@ -186,7 +186,7 @@ class WelcomePage extends AbstractWelcomePage { ? 'with-content' : 'without-content'}` } id = 'welcome_page'> - +
@@ -204,7 +204,7 @@ class WelcomePage extends AbstractWelcomePage {

{ t('welcomepage.enterRoomTitle') }

- {/*

+ {/*

{ t('welcomepage.subTitle') }

@@ -220,8 +220,9 @@ class WelcomePage extends AbstractWelcomePage { className = 'enter-room-input' id = 'enter_room_field' onChange = { this._onRoomNameChanged } - //pattern = { ROOM_NAME_VALIDATE_PATTERN_STR } - placeholder = { t('welcomepage.placeholderEnterRoomName') } //this.state.roomPlaceholder + + // pattern = { ROOM_NAME_VALIDATE_PATTERN_STR } + placeholder = { t('welcomepage.placeholderEnterRoomName') } // this.state.roomPlaceholder ref = { this._setRoomInputRef } title = { t('welcomepage.roomNameAllowedChars') } type = 'text' @@ -230,7 +231,7 @@ class WelcomePage extends AbstractWelcomePage {

{ @@ -240,7 +241,7 @@ class WelcomePage extends AbstractWelcomePage { }
-
{ t('welcomepage.startSession') }
+
{ t('welcomepage.startSession') }
{/* this._renderTabs() */} { showAdditionalContent