]+>/i;
+ ChatLogger.debug('TimeWindowChatItem::renderMessageItem', this.props);
+ const defaultAvatarString = name?.toLowerCase().slice(0, 2) || ' ';
+ const shouldRenderPrivateMessageReadFeedback =
+ isMessageReadFeedbackEnabled &&
+ chatId !== CHAT_PUBLIC_ID &&
+ isFromMe &&
+ lastReadByPartnerMessageTime >= messages[messages.length - 1].time;
+ const emphasizedText = messageFromModerator && CHAT_EMPHASIZE_TEXT && chatId === CHAT_PUBLIC_ID;
+
+ return (
+ this.itemRef = element}
+ >
+
+
+
+ {defaultAvatarString}
+
+
+
+
+
+ {name}
+ {isOnline
+ ? null
+ : (
+
+ {`(${intl.formatMessage(intlMessages.offline)})`}
+
+ )}
+
+
+
+
+ {shouldRenderPrivateMessageReadFeedback
+ && (
+
+
+
+ )}
+
+
+ {messages.map((message) => (
+ {
+ if (!read) {
+ dispatch({
+ type: 'last_read_message_timestamp_changed',
+ value: {
+ chatId,
+ timestamp,
+ },
+ });
+ }
+ }}
+ scrollArea={scrollArea}
+ />
+ ))}
+
+
+
+
+ );
+ }
+
+ renderStatusItem() {
+ const {
+ timestamp,
+ color,
+ intl,
+ messages,
+ scrollArea,
+ chatAreaId,
+ messageKey,
+ dispatch,
+ chatId,
+ extra,
+ read,
+ name,
+ messageFromModerator,
+ avatar,
+ isOnline,
+ } = this.props;
+
+ const dateTime = new Date(timestamp);
+ ChatLogger.debug('TimeWindowChatItem::renderMessageItem', this.props);
+ const defaultAvatarString = name?.toLowerCase().slice(0, 2) || ' ';
+ const emphasizedTextClass = messageFromModerator && CHAT_EMPHASIZE_TEXT
+ && chatId === CHAT_PUBLIC_ID;
+
+ return messages ? (
+
+
+
+
+ {defaultAvatarString}
+
+
+
+
+
+ {name}
+ {isOnline
+ ? null
+ : (
+
+ {`(${intl.formatMessage(intlMessages.offline)})`}
+
+ )}
+
+
+
+
+
+
+ {messages.map((message) => {
+ const isSystemMsg = message.id === `SYSTEM_MESSAGE-${CHAT_USER_STATUS_MESSAGE}`;
+ return (
+ ${intl.formatMessage(intlMessages.away)}`
+ : ` ${intl.formatMessage(intlMessages.notAway)}`
+ : message.text}
+ time={message.time}
+ chatAreaId={chatAreaId}
+ dispatch={dispatch}
+ read={message.read}
+ chatUserMessageItem
+ handleReadMessage={(time) => {
+ if (!read) {
+ dispatch({
+ type: 'last_read_message_timestamp_changed',
+ value: {
+ chatId,
+ timestamp: time,
+ },
+ });
+ }
+ }}
+ scrollArea={scrollArea}
+ />
+ );
+ })}
+
+
+
+
+ ) : null;
+ }
+
+ renderPollItem() {
+ const {
+ timestamp,
+ color,
+ intl,
+ getPollResultString,
+ messages,
+ extra,
+ scrollArea,
+ chatAreaId,
+ lastReadMessageTime,
+ dispatch,
+ chatId,
+ read,
+ } = this.props;
+
+ const dateTime = new Date(timestamp);
+
+ return messages ? (
+
+ { this.item = ref; }}>
+
+
+ { }
+
+
+
+
+
+ {intl.formatMessage(intlMessages.pollResult)}
+
+
+
+
+
+ {
+ if (!read) {
+ dispatch({
+ type: 'last_read_message_timestamp_changed',
+ value: {
+ chatId,
+ timestamp: time,
+ },
+ });
+ }
+ }}
+ scrollArea={scrollArea}
+ color={color}
+ />
+
+
+
+ ) : null;
+ }
+
+ renderExportedPresentationItem() {
+ const {
+ timestamp,
+ color,
+ intl,
+ messages,
+ extra,
+ scrollArea,
+ chatAreaId,
+ lastReadMessageTime,
+ handleReadMessage,
+ dispatch,
+ read,
+ chatId,
+ getExportedPresentationString,
+ } = this.props;
+
+ const dateTime = new Date(timestamp);
+
+ return messages ? (
+ { e.stopPropagation(); }}
+ >
+ { this.item = ref; }}>
+
+
+
+
+
+
+
+
+
+
+
+ {
+ handleReadMessage(timestamp);
+
+ if (!read) {
+ dispatch({
+ type: 'last_read_message_timestamp_changed',
+ value: {
+ chatId,
+ timestamp,
+ },
+ });
+ }
+ }}
+ scrollArea={scrollArea}
+ color={color}
+ />
+
+
+
+ ) : null;
+ }
+
+ render() {
+ const {
+ systemMessage,
+ } = this.props;
+ ChatLogger.debug('TimeWindowChatItem::render', { ...this.props });
+ if (systemMessage) {
+ return this.renderSystemMessage();
+ }
+
+ return this.renderMessageItem();
+ }
+}
+
+TimeWindowChatItem.propTypes = propTypes;
+TimeWindowChatItem.defaultProps = defaultProps;
+
+export default injectIntl(TimeWindowChatItem);
diff --git a/src/2.7.12/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx b/src/2.7.12/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx
new file mode 100644
index 00000000..6e25a936
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx
@@ -0,0 +1,69 @@
+import React, { useContext } from 'react';
+import TimeWindowChatItem from './component';
+import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
+import ChatService from '../../service';
+import { layoutSelect } from '../../../layout/context';
+import PollService from '/imports/ui/components/poll/service';
+import Auth from '/imports/ui/services/auth';
+
+const CHAT_CONFIG = Meteor.settings.public.chat;
+const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
+const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
+
+const TimeWindowChatItemContainer = (props) => {
+ const { message, messageId } = props;
+
+ const idChatOpen = layoutSelect((i) => i.idChatOpen);
+
+ const usingUsersContext = useContext(UsersContext);
+ const { users } = usingUsersContext;
+ const {
+ sender,
+ senderName,
+ senderRole,
+ key,
+ timestamp,
+ content,
+ extra,
+ messageValues,
+ } = message;
+ const messages = content;
+
+ const user = (sender === 'SYSTEM') ? {
+ name: senderName,
+ color: '#01579b',
+ avatar: '',
+ role: ROLE_MODERATOR,
+ loggedOut: false,
+ } : users[Auth.meetingID][sender];
+ const messageKey = key;
+ const handleReadMessage = (tstamp) => ChatService.updateUnreadMessage(tstamp, idChatOpen);
+ return (
+
+ );
+};
+
+export default TimeWindowChatItemContainer;
diff --git a/src/2.7.12/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/component.jsx b/src/2.7.12/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/component.jsx
new file mode 100644
index 00000000..d85bce85
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/component.jsx
@@ -0,0 +1,189 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { debounce } from '/imports/utils/debounce';
+import fastdom from 'fastdom';
+import { injectIntl } from 'react-intl';
+import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
+
+const propTypes = {
+ text: PropTypes.string.isRequired,
+ time: PropTypes.number.isRequired,
+ lastReadMessageTime: PropTypes.number,
+ handleReadMessage: PropTypes.func.isRequired,
+ scrollArea: PropTypes.instanceOf(Element),
+ className: PropTypes.string.isRequired,
+};
+
+const defaultProps = {
+ lastReadMessageTime: 0,
+ scrollArea: undefined,
+};
+
+const eventsToBeBound = [
+ 'scroll',
+ 'resize',
+];
+
+const isElementInViewport = (el) => {
+ if (!el) return false;
+ const rect = el.getBoundingClientRect();
+
+ return (
+ rect.top >= 0
+ // This condition is for large messages that are bigger than client height
+ || rect.top + rect.height >= 0
+ );
+};
+
+class MessageChatItem extends PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.ticking = false;
+
+ this.handleMessageInViewport = debounce(this.handleMessageInViewport.bind(this), 50);
+ }
+
+ componentDidMount() {
+ this.listenToUnreadMessages();
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ ChatLogger.debug('MessageChatItem::componentDidUpdate::props', { ...this.props }, { ...prevProps });
+ ChatLogger.debug('MessageChatItem::componentDidUpdate::state', { ...this.state }, { ...prevState });
+ this.listenToUnreadMessages();
+ }
+
+ componentWillUnmount() {
+ // This was added 3 years ago, but never worked. Leaving it around in case someone returns
+ // and decides it needs to be fixed like the one in listenToUnreadMessages()
+ // if (!lastReadMessageTime > time) {
+ // return;
+ // }
+ ChatLogger.debug('MessageChatItem::componentWillUnmount', this.props);
+ this.removeScrollListeners();
+ }
+
+ addScrollListeners() {
+ const {
+ scrollArea,
+ } = this.props;
+
+ if (scrollArea) {
+ eventsToBeBound.forEach(
+ e => scrollArea.addEventListener(e, this.handleMessageInViewport),
+ );
+ }
+ }
+
+ handleMessageInViewport() {
+ if (!this.ticking) {
+ fastdom.measure(() => {
+ const node = this.text;
+ const {
+ handleReadMessage,
+ time,
+ read,
+ } = this.props;
+
+ if (read) {
+ this.removeScrollListeners();
+ return;
+ }
+
+ if (isElementInViewport(node)) {
+ handleReadMessage(time);
+ this.removeScrollListeners();
+ }
+
+ this.ticking = false;
+ });
+ }
+
+ this.ticking = true;
+ }
+
+ removeScrollListeners() {
+ const {
+ scrollArea,
+ read,
+ } = this.props;
+
+ if (scrollArea && !read) {
+ eventsToBeBound.forEach(
+ e => scrollArea.removeEventListener(e, this.handleMessageInViewport),
+ );
+ }
+ }
+
+ // depending on whether the message is in viewport or not,
+ // either read it or attach a listener
+ listenToUnreadMessages() {
+ const {
+ handleReadMessage,
+ time,
+ read,
+ } = this.props;
+
+ if (read) {
+ return;
+ }
+
+ const node = this.text;
+
+ fastdom.measure(() => {
+ const {
+ read: newRead,
+ } = this.props;
+ // this function is called after so we need to get the updated lastReadMessageTime
+
+ if (newRead) {
+ return;
+ }
+
+ if (isElementInViewport(node)) { // no need to listen, the message is already in viewport
+ handleReadMessage(time);
+ } else {
+ this.addScrollListeners();
+ }
+ });
+ }
+
+ render() {
+ const {
+ text,
+ type,
+ className,
+ isSystemMessage,
+ chatUserMessageItem,
+ systemMessageType,
+ color,
+ } = this.props;
+ ChatLogger.debug('MessageChatItem::render', this.props);
+ if (type === 'poll') {
+ return (
+ { this.text = ref; }}
+ dangerouslySetInnerHTML={{ __html: text }}
+ data-test="chatPollMessageText"
+ />
+ );
+ } else {
+ return (
+
{ this.text = ref; }}
+ dangerouslySetInnerHTML={{ __html: text }}
+ data-test={isSystemMessage ? systemMessageType : chatUserMessageItem ? 'chatUserMessageText' : ''}
+ />
+ );
+ }
+ }
+}
+
+MessageChatItem.propTypes = propTypes;
+MessageChatItem.defaultProps = defaultProps;
+
+export default injectIntl(MessageChatItem);
diff --git a/src/2.7.12/imports/ui/components/chat/time-window-list/time-window-chat-item/styles.js b/src/2.7.12/imports/ui/components/chat/time-window-list/time-window-chat-item/styles.js
new file mode 100644
index 00000000..ea92693d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/chat/time-window-list/time-window-chat-item/styles.js
@@ -0,0 +1,285 @@
+import styled from 'styled-components';
+import {
+ borderRadius,
+ borderSize,
+ chatPollMarginSm,
+} from '/imports/ui/stylesheets/styled-components/general';
+import { lineHeightComputed, fontSizeBase, btnFontWeight } from '/imports/ui/stylesheets/styled-components/typography';
+import {
+ systemMessageBackgroundColor,
+ systemMessageBorderColor,
+ systemMessageFontColor,
+ highlightedMessageBackgroundColor,
+ highlightedMessageBorderColor,
+ colorHeading,
+ colorGrayLight,
+ palettePlaceholderText,
+ colorGrayLighter,
+ colorPrimary,
+ colorText,
+ colorSuccess,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import MessageChatItem from './message-chat-item/component';
+import Icon from '/imports/ui/components/common/icon/component';
+
+const Item = styled.div`
+ padding: calc(${lineHeightComputed} / 4) 0 calc(${lineHeightComputed} / 2) 0;
+ font-size: ${fontSizeBase};
+ pointer-events: auto;
+ [dir="rtl"] & {
+ direction: rtl;
+ }
+`;
+
+const Messages = styled.div`
+ > * {
+ &:first-child {
+ margin-top: calc(${lineHeightComputed} / 4);
+ }
+ }
+`;
+
+const StatusMessageChatItem = styled(MessageChatItem)`
+ background: ${systemMessageBackgroundColor};
+ border: 1px solid ${systemMessageBorderColor};
+ border-radius: ${borderRadius};
+ font-weight: ${btnFontWeight};
+ padding: ${fontSizeBase};
+ color: ${systemMessageFontColor};
+ margin-top: 0;
+ margin-bottom: 0;
+ overflow-wrap: break-word;
+ ${({ isSystemMsg }) => !isSystemMsg && `
+ flex: 1;
+ margin-top: calc(${lineHeightComputed} / 3);
+ margin-bottom: 0;
+ color: ${colorText};
+ word-wrap: break-word;
+ background: unset;
+ border: unset;
+ border-radius: unset;
+ font-weight: unset;
+ padding: unset;
+ `}
+ ${({ emphasizedMessage }) => emphasizedMessage && `
+ font-weight: bold;
+ `}
+`;
+
+const SystemMessageChatItem = styled(MessageChatItem)`
+ ${({ border }) => border && `
+ background-color: ${systemMessageBackgroundColor};
+ border: 1px solid ${systemMessageBorderColor};
+ border-radius: ${borderRadius};
+ font-weight: ${btnFontWeight};
+ padding: ${fontSizeBase};
+ color: ${systemMessageFontColor};
+ margin-top: 0px;
+ margin-bottom: 0px;
+ overflow-wrap: break-word;
+ `}
+
+ ${({ border }) => !border && `
+ color: ${systemMessageFontColor};
+ margin-top: 0px;
+ margin-bottom: 0px;
+ `}
+`;
+
+const Wrapper = styled.div`
+ display: flex;
+ flex-flow: row;
+ flex: 1;
+ position: relative;
+ margin: ${borderSize} 0 0 ${borderSize};
+
+ ${({ isSystemSender }) => isSystemSender && `
+ background-color: ${highlightedMessageBackgroundColor};
+ border-left: 2px solid ${highlightedMessageBorderColor};
+ border-radius: 0px 3px 3px 0px;
+ padding: 8px 2px;
+ `}
+
+ [dir="rtl"] & {
+ margin: ${borderSize} ${borderSize} 0 0;
+ }
+`;
+
+const AvatarWrapper = styled.div`
+ flex-basis: 2.25rem;
+ flex-shrink: 0;
+ flex-grow: 0;
+ margin: 0 calc(${lineHeightComputed} / 2) 0 0;
+
+ [dir="rtl"] & {
+ margin: 0 0 0 calc(${lineHeightComputed} / 2);
+ }
+`;
+
+const Content = styled.div`
+ flex: 1;
+ display: flex;
+ flex-flow: column;
+ overflow-x: hidden;
+ width: calc(100% - 1.7rem);
+`;
+
+const Meta = styled.div`
+ display: flex;
+ flex: 1;
+ flex-flow: row;
+ line-height: 1.35;
+ align-items: baseline;
+`;
+
+const Name = styled.div`
+ display: flex;
+ min-width: 0;
+ font-weight: 600;
+ position: relative;
+
+ &:first-child {
+ min-width: 0;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ ${({ isOnline }) => isOnline && `
+ color: ${colorHeading};
+ `}
+
+ ${({ isOnline }) => !isOnline && `
+ text-transform: capitalize;
+ font-style: italic;
+
+ & > span {
+ text-align: right;
+ padding: 0 .1rem 0 0;
+
+ [dir="rtl"] & {
+ text-align: left;
+ padding: 0 0 0 .1rem;
+ }
+ }
+ `}
+`;
+
+const Offline = styled.span`
+ color: ${colorGrayLight};
+ font-weight: 100;
+ text-transform: lowercase;
+ font-style: italic;
+ font-size: 90%;
+ line-height: 1;
+ align-self: center;
+`;
+
+const Time = styled.time`
+ flex-shrink: 0;
+ flex-grow: 0;
+ flex-basis: 3.5rem;
+ color: ${palettePlaceholderText};
+ text-transform: uppercase;
+ font-size: 75%;
+ margin: 0 0 0 calc(${lineHeightComputed} / 2);
+
+ [dir="rtl"] & {
+ margin: 0 calc(${lineHeightComputed} / 2) 0 0;
+ }
+
+ & > span {
+ vertical-align: sub;
+ }
+`;
+
+const ChatItem = styled(MessageChatItem)`
+ flex: 1;
+ margin-top: calc(${lineHeightComputed} / 3);
+ margin-bottom: 0;
+ color: ${colorText};
+ word-wrap: break-word;
+
+ ${({ hasLink }) => hasLink && `
+ & > a {
+ color: ${colorPrimary};
+ }
+ `}
+
+ ${({ emphasizedMessage }) => emphasizedMessage && `
+ font-weight: bold;
+ `}
+`;
+
+const PollIcon = styled(Icon)`
+ bottom: 1px;
+`;
+
+const PollMessageChatItem = styled(MessageChatItem)`
+ flex: 1;
+ margin-top: calc(${lineHeightComputed} / 3);
+ margin-bottom: 0;
+ color: ${colorText};
+ word-wrap: break-word;
+
+ background-color: ${systemMessageBackgroundColor};
+ border: solid 1px ${colorGrayLighter};
+ border-radius: ${borderRadius};
+ padding: ${chatPollMarginSm};
+ padding-left: 1rem;
+ margin-top: ${chatPollMarginSm} !important;
+`;
+
+const PresentationWrapper = styled(Wrapper)`
+ display: flex;
+ flex-flow: row;
+ flex: 1;
+ position: relative;
+ margin: ${borderSize} 0 0 ${borderSize};
+ border-left: 2px solid ${colorPrimary};
+ border-radius: 2px;
+ padding: 6px 0 6px 6px;
+ background-color: ${systemMessageBackgroundColor};
+
+ [dir="rtl"] & {
+ margin: ${borderSize} ${borderSize} 0 0;
+ border-right: 2px solid ${colorPrimary};
+ border-left: none;
+ }
+`;
+
+const PresentationChatItem = styled(MessageChatItem)`
+ flex: 1;
+ margin-top: ${chatPollMarginSm};
+ margin-bottom: 0;
+ color: ${colorText};
+ word-wrap: break-word;
+`;
+
+const ReadIcon = styled(Icon)`
+ color: ${colorSuccess};
+ align-self: center;
+ margin-left: auto;
+ margin-right: .5rem;
+`;
+
+export default {
+ Item,
+ Messages,
+ SystemMessageChatItem,
+ Wrapper,
+ AvatarWrapper,
+ Content,
+ Meta,
+ Name,
+ Offline,
+ Time,
+ ChatItem,
+ PollIcon,
+ PollMessageChatItem,
+ PresentationChatItem,
+ PresentationWrapper,
+ StatusMessageChatItem,
+ ReadIcon,
+};
diff --git a/src/2.7.12/imports/ui/components/click-outside/component.jsx b/src/2.7.12/imports/ui/components/click-outside/component.jsx
new file mode 100644
index 00000000..d7f01883
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/click-outside/component.jsx
@@ -0,0 +1,39 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ onClick: PropTypes.func.isRequired,
+}
+
+class ClickOutside extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.childrenRefs = React.Children.map(this.props.children, () => React.createRef());
+ }
+
+ componentDidMount() {
+ document.addEventListener("click", this.handleClick);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("click", this.handleClick);
+ }
+
+ handleClick = (e) => {
+ const { onClick } = this.props;
+ const isOutside = this.childrenRefs.every(ref => !ref.current.contains(e.target));
+ if (isOutside) {
+ onClick();
+ }
+ };
+
+ render() {
+ return React.Children.map(this.props.children, (element, idx) => {
+ return React.cloneElement(element, { ref: this.childrenRefs[idx] });
+ });
+ }
+}
+
+ClickOutside.propTypes = propTypes;
+
+export default ClickOutside;
diff --git a/src/2.7.12/imports/ui/components/common/button/base/component.jsx b/src/2.7.12/imports/ui/components/common/button/base/component.jsx
new file mode 100644
index 00000000..347cfb9e
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/button/base/component.jsx
@@ -0,0 +1,197 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { omit } from 'radash';
+
+const propTypes = {
+ /**
+ * Defines HTML disable Attribute
+ * @defaultValue false
+ */
+ disabled: PropTypes.bool,
+
+ /**
+ * Defines HTML Tag
+ * @defaultValue 'button'
+ */
+ tagName: PropTypes.string,
+
+ /**
+ * Defines the button label
+ * @defaultValue undefined
+ */
+ label: PropTypes.string.isRequired,
+
+ /**
+ * Defines the button click handler
+ * @defaultValue undefined
+ */
+ onClick: (props, propName, componentName) => {
+ if (!props.onClick && !props.onMouseDown && !props.onMouseUp) {
+ return new Error('One of props \'onClick\' or \'onMouseDown\' or' +
+ ` 'onMouseUp' was not specified in '${componentName}'.`);
+ }
+
+ return null;
+ },
+ onMouseDown: (props, propName, componentName) => {
+ if (!props.onClick && !props.onMouseDown && !props.onMouseUp) {
+ return new Error('One of props \'onClick\' or \'onMouseDown\' or' +
+ ` 'onMouseUp' was not specified in '${componentName}'.`);
+ }
+
+ return null;
+ },
+ onMouseUp: (props, propName, componentName) => {
+ if (!props.onClick && !props.onMouseDown && !props.onMouseUp) {
+ return new Error('One of props \'onClick\' or \'onMouseDown\' or' +
+ ` 'onMouseUp' was not specified in '${componentName}'.`);
+ }
+
+ return null;
+ },
+
+ onKeyPress: PropTypes.func,
+ onKeyDown: PropTypes.func,
+ onKeyUp: PropTypes.func,
+ setRef: PropTypes.func,
+};
+
+const defaultProps = {
+ disabled: false,
+ tagName: 'button',
+ onClick: undefined,
+ onMouseDown: undefined,
+ onMouseUp: undefined,
+ onKeyPress: undefined,
+ onKeyDown: undefined,
+ onKeyUp: undefined,
+ setRef: undefined,
+};
+
+/**
+ * Event handlers below are used to intercept a parent event handler from
+ * firing when the Button is disabled.
+ * Key press event handlers intercept firing for
+ * keyboard users to comply with ARIA standards.
+ */
+
+export default class ButtonBase extends React.Component {
+ constructor(props) {
+ super(props);
+
+ // Bind Mouse Event Handlers
+ this.internalClickHandler = this.internalClickHandler.bind(this);
+ this.internalDoubleClickHandler = this.internalDoubleClickHandler.bind(this);
+ this.internalMouseDownHandler = this.internalMouseDownHandler.bind(this);
+ this.internalMouseUpHandler = this.internalMouseUpHandler.bind(this);
+
+ // Bind Keyboard Event Handlers
+ this.internalKeyPressHandler = this.internalKeyPressHandler.bind(this);
+ this.internalKeyDownHandler = this.internalKeyDownHandler.bind(this);
+ this.internalKeyUpHandler = this.internalKeyUpHandler.bind(this);
+ }
+
+ validateDisabled(eventHandler, ...args) {
+ if (!this.props.disabled && typeof eventHandler === 'function') {
+ return eventHandler(...args);
+ }
+
+ return null;
+ }
+
+ // Define Mouse Event Handlers
+ internalClickHandler(...args) {
+ return this.validateDisabled(this.props.onClick, ...args);
+ }
+
+ internalDoubleClickHandler(...args) {
+ return this.validateDisabled(this.props.onDoubleClick, ...args);
+ }
+
+ internalMouseDownHandler(...args) {
+ return this.validateDisabled(this.props.onMouseDown, ...args);
+ }
+
+ internalMouseUpHandler(...args) {
+ return this.validateDisabled(this.props.onMouseUp, ...args);
+ }
+
+ // Define Keyboard Event Handlers
+ internalKeyPressHandler(...args) {
+ return this.validateDisabled(this.props.onKeyPress, ...args);
+ }
+
+ internalKeyDownHandler(...args) {
+ return this.validateDisabled(this.props.onKeyDown, ...args);
+ }
+
+ internalKeyUpHandler(...args) {
+ return this.validateDisabled(this.props.onKeyUp, ...args);
+ }
+
+ render() {
+ const Component = this.props.tagName;
+
+ const remainingProps = Object.assign({}, this.props);
+ delete remainingProps.label;
+ delete remainingProps.tagName;
+ delete remainingProps.disabled;
+
+ // Delete Mouse event handlers
+ delete remainingProps.onClick;
+ delete remainingProps.onDoubleClick;
+ delete remainingProps.onMouseDown;
+ delete remainingProps.onMouseUp;
+
+ // Delete Keyboard event handlers
+ delete remainingProps.onKeyPress;
+ delete remainingProps.onKeyDown;
+ delete remainingProps.onKeyUp;
+
+ // Delete setRef callback if it exists
+ delete remainingProps.setRef;
+
+ const styleProps = [
+ 'ghost',
+ 'circle',
+ 'block',
+ 'hasNotification',
+ 'isStyled',
+ 'isDownloadable',
+ 'animations',
+ 'small',
+ 'full',
+ 'iconRight',
+ 'isVisualEffects',
+ 'panning',
+ 'panSelected',
+ ];
+
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+ButtonBase.propTypes = propTypes;
+ButtonBase.defaultProps = defaultProps;
diff --git a/src/2.7.12/imports/ui/components/common/button/button-emoji/ButtonEmoji.jsx b/src/2.7.12/imports/ui/components/common/button/button-emoji/ButtonEmoji.jsx
new file mode 100644
index 00000000..13a3710f
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/button/button-emoji/ButtonEmoji.jsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Styled from './styles';
+import TooltipContainer from '/imports/ui/components/common/tooltip/container';
+
+const propTypes = {
+ /**
+ * Defines the name of the emoji to be used, as defined in bbb-icons.css
+ * @type String
+ * @defaultValue ''
+ */
+ emoji: PropTypes.string,
+
+ label: PropTypes.string,
+
+ onClick: PropTypes.func,
+
+ onKeyDown: PropTypes.func,
+
+ onFocus: PropTypes.func,
+
+ tabIndex: PropTypes.number,
+
+ hideLabel: PropTypes.bool,
+
+ className: PropTypes.string,
+
+ rotate: PropTypes.bool,
+};
+
+const defaultProps = {
+ emoji: '',
+ label: '',
+ onKeyDown: null,
+ onFocus: null,
+ tabIndex: -1,
+ hideLabel: false,
+ onClick: null,
+ className: '',
+ rotate: false,
+};
+
+const ButtonEmoji = (props) => {
+ const {
+ hideLabel,
+ className,
+ hidden,
+ rotate,
+ ...newProps
+ } = props;
+
+ const {
+ emoji,
+ label,
+ tabIndex,
+ onClick,
+ } = newProps;
+
+ const IconComponent = ( );
+
+ return (
+
+
+
+
+
+ { !hideLabel && label }
+ { IconComponent }
+
+
+
+
+ );
+};
+
+export default ButtonEmoji;
+
+ButtonEmoji.propTypes = propTypes;
+ButtonEmoji.defaultProps = defaultProps;
diff --git a/src/2.7.12/imports/ui/components/common/button/button-emoji/styles.js b/src/2.7.12/imports/ui/components/common/button/button-emoji/styles.js
new file mode 100644
index 00000000..3eb4fbf5
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/button/button-emoji/styles.js
@@ -0,0 +1,91 @@
+import styled from 'styled-components';
+import Icon from '/imports/ui/components/common/icon/component';
+import {
+ btnDefaultColor,
+ btnDefaultBg,
+ colorBackground,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import { btnSpacing } from '/imports/ui/stylesheets/styled-components/general';
+
+const EmojiButtonIcon = styled(Icon)`
+ position: absolute;
+ top: 0;
+ height: 40%;
+ left: 0;
+ width: 75%;
+ margin-left: 14%;
+ font-size: 50%;
+ margin-top: 30%;
+ color: ${btnDefaultColor};
+`;
+
+const Label = styled.span`
+ & + i,
+ & + button {
+ margin: 0 0 0 ${btnSpacing};
+
+ [dir="rtl"] & {
+ margin: 0 ${btnSpacing} 0 0;
+ }
+ }
+ &:hover {
+ opacity: .5;
+ }
+`;
+
+const EmojiButton = styled.button`
+ position: absolute;
+ border-radius: 50%;
+ width: 1em;
+ height: 1em;
+ right: -.2em;
+ bottom: 0;
+ background-color: ${btnDefaultBg};
+ overflow: hidden;
+ z-index: 2;
+ border: none;
+ padding: 0;
+
+ [dir="rtl"] & {
+ right: initial;
+ left: -.2em;
+ }
+
+ &:hover {
+ transform: scale(1.5);
+ transition-duration: 150ms;
+ }
+
+ & > i {
+ position: absolute;
+ top: 0;
+ height: 60%;
+ left: 0;
+ margin-left: 25%;
+ font-size: 50%;
+ margin-top: 40%;
+ color: ${btnDefaultColor};
+ }
+`;
+
+const EmojiButtonSpace = styled.div`
+ position: absolute;
+ height: 1.4em;
+ width: 1.4em;
+ background-color: ${colorBackground};
+ right: -.4em;
+ bottom: -.2em;
+ border-radius: 50%;
+
+ [dir="rtl"] & {
+ right: initial;
+ left: -.4em;
+ }
+`;
+
+export default {
+ EmojiButtonIcon,
+ Label,
+ EmojiButton,
+ EmojiButtonSpace,
+};
diff --git a/src/2.7.12/imports/ui/components/common/button/component.jsx b/src/2.7.12/imports/ui/components/common/button/component.jsx
new file mode 100644
index 00000000..c5327a48
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/button/component.jsx
@@ -0,0 +1,257 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import TooltipContainer from '/imports/ui/components/common/tooltip/container';
+import Styled from './styles';
+import BaseButton from './base/component';
+import ButtonEmoji from './button-emoji/ButtonEmoji';
+
+const SIZES = [
+ 'jumbo', 'lg', 'md', 'sm',
+];
+
+const COLORS = [
+ 'default', 'primary', 'danger', 'warning', 'success', 'dark', 'light', 'offline', 'muted', 'secondary',
+];
+
+const propTypes = {
+ ...BaseButton.propTypes,
+ /**
+ * Defines the button size style
+ * @type {("lg"|"md"|"sm")}
+ * @defaultValue 'md'
+ */
+ size: PropTypes.oneOf(SIZES),
+
+ /**
+ * Defines the button color style
+ * @type {("default"|"primary"|"danger"|"success")}
+ * @defaultValue 'md'
+ */
+ color: PropTypes.oneOf(COLORS),
+
+ /**
+ * Defines if the button should be styled as a ghost (outline)
+ * @defaultValue false
+ */
+ ghost: PropTypes.bool,
+
+ /**
+ * Defines if the button should be styled as circle
+ * @defaultValue false
+ */
+ circle: PropTypes.bool,
+
+ /**
+ * Defines if the button should have `display: block`
+ * @defaultValue false
+ */
+ block: PropTypes.bool,
+
+ /**
+ * Defines the button icon
+ * @defaultValue undefined
+ */
+ icon: PropTypes.string,
+
+ /**
+ * Defines the button icon is on the right side
+ * @defaultValue false
+ */
+ iconRight: PropTypes.bool,
+
+ /**
+ * Defines the button label should be visible
+ * @defaultValue false
+ */
+ hideLabel: PropTypes.bool,
+
+ /**
+ * Optional SVG / html object can be passed to the button as an icon
+ * Has to be styled before being sent to the Button
+ * (e.g width, height, position and percentage-based object's coordinates)
+ * @defaultvalue undefined
+ */
+ customIcon: PropTypes.node,
+};
+
+const defaultProps = {
+ ...BaseButton.defaultProps,
+ size: 'md',
+ color: 'default',
+ ghost: false,
+ circle: false,
+ block: false,
+ iconRight: false,
+ hideLabel: false,
+ tooltipLabel: '',
+};
+
+export default class Button extends BaseButton {
+ _cleanProps(otherProps) {
+ const remainingProps = Object.assign({}, otherProps);
+ delete remainingProps.icon;
+ delete remainingProps.customIcon;
+ delete remainingProps.size;
+ delete remainingProps.color;
+ delete remainingProps.ghost;
+ delete remainingProps.circle;
+ delete remainingProps.block;
+ delete remainingProps.hideLabel;
+ delete remainingProps.tooltipLabel;
+
+ return remainingProps;
+ }
+
+ hasButtonEmojiComponent() {
+ const { children } = this.props;
+
+ if (!children) return false;
+
+ const buttonEmoji = React.Children.only(children);
+
+ return (buttonEmoji && buttonEmoji.type && buttonEmoji.type.name)
+ ? (buttonEmoji.type.name === ButtonEmoji.name)
+ : false;
+ }
+
+ render() {
+ const {
+ circle,
+ hideLabel,
+ label,
+ 'aria-label': ariaLabel,
+ 'aria-expanded': ariaExpanded,
+ tooltipLabel,
+ tooltipdelay,
+ tooltipplacement,
+ } = this.props;
+
+ const renderFuncName = circle ? 'renderCircle' : 'renderDefault';
+
+ if ((hideLabel && !ariaExpanded) || tooltipLabel) {
+ const buttonLabel = label || ariaLabel;
+ return (
+
+ {this[renderFuncName]()}
+
+ );
+ }
+
+ return this[renderFuncName]();
+ }
+
+ renderDefault() {
+ const {
+ className,
+ iconRight,
+ size,
+ color,
+ ghost,
+ circle,
+ block,
+ ...otherProps
+ } = this.props;
+
+ const remainingProps = this._cleanProps(otherProps);
+
+ return (
+
+ {this.renderIcon()}
+ {this.renderLabel()}
+
+ );
+ }
+
+ renderCircle() {
+ const {
+ className,
+ size,
+ iconRight,
+ children,
+ color,
+ ghost,
+ circle,
+ block,
+ ...otherProps
+ } = this.props;
+
+ const remainingProps = this._cleanProps(otherProps);
+
+ return (
+
+ {this.renderButtonEmojiSibling()}
+ {!iconRight ? null : this.renderLabel()}
+
+ {this.renderIcon()}
+
+ {iconRight ? null : this.renderLabel()}
+ {this.hasButtonEmojiComponent() ? children : null}
+
+ );
+ }
+
+ renderButtonEmojiSibling() {
+ if (!this.hasButtonEmojiComponent()) {
+ return null;
+ }
+
+ return ( );
+ }
+
+ renderIcon() {
+ const {
+ icon: iconName,
+ customIcon,
+ } = this.props;
+
+ if (iconName) {
+ return ( );
+ } if (customIcon) {
+ return customIcon;
+ }
+
+ return null;
+ }
+
+ renderLabel() {
+ const { label, hideLabel } = this.props;
+
+ return (
+
+ {label}
+ {!this.hasButtonEmojiComponent() ? this.props.children : null}
+
+ );
+ }
+}
+
+Button.propTypes = propTypes;
+Button.defaultProps = defaultProps;
diff --git a/src/2.7.12/imports/ui/components/common/button/styles.js b/src/2.7.12/imports/ui/components/common/button/styles.js
new file mode 100644
index 00000000..1f1f8e00
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/button/styles.js
@@ -0,0 +1,1223 @@
+import styled from 'styled-components';
+import Icon from '/imports/ui/components/common/icon/component';
+import {
+ btnSpacing,
+ borderRadius,
+ borderSizeSmall,
+ borderSize,
+ borderSizeLarge,
+ smPaddingY,
+ smPaddingX,
+ mdPaddingY,
+ mdPaddingX,
+ lgPaddingY,
+ lgPaddingX,
+ jumboPaddingY,
+ jumboPaddingX,
+} from '/imports/ui/stylesheets/styled-components/general';
+import {
+ fontSizeSmall,
+ fontSizeBase,
+ fontSizeLarge,
+ btnFontWeight,
+} from '/imports/ui/stylesheets/styled-components/typography';
+import {
+ btnDefaultColor,
+ btnDefaultBg,
+ btnDefaultGhostColor,
+ btnDefaultGhostBg,
+ btnDefaultGhostActiveBg,
+ btnDefaultGhostBorder,
+ btnPrimaryBorder,
+ btnPrimaryColor,
+ btnPrimaryBg,
+ btnPrimaryHoverBg,
+ btnPrimaryActiveBg,
+ btnSuccessBorder,
+ btnSuccessColor,
+ btnSuccessBg,
+ btnWarningBorder,
+ btnWarningColor,
+ btnWarningBg,
+ btnDangerBorder,
+ btnDangerColor,
+ btnDangerBg,
+ btnDangerBgHover,
+ btnDarkBorder,
+ btnDarkColor,
+ btnDarkBg,
+ btnOfflineBorder,
+ btnOfflineColor,
+ btnOfflineBg,
+ btnMutedBorder,
+ btnMutedColor,
+ btnMutedBg,
+ colorWhite,
+ colorGray,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import BaseButton from './base/component';
+
+const ButtonIcon = styled(Icon)`
+ width: 1em;
+ height: 1em;
+ text-align: center;
+
+ &:before {
+ width: 1em;
+ height: 1em;
+ }
+
+ .buttonWrapper & {
+ font-size: 125%;
+ }
+
+ & + span {
+ margin: 0 0 0 ${btnSpacing};
+
+ [dir="rtl"] & {
+ margin: 0 ${btnSpacing} 0 0;
+ }
+ }
+`;
+
+const EmojiButtonSibling = styled.span`
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ z-index: 0;
+`;
+
+const ButtonLabel = styled.span`
+ & + i,
+ & + button {
+ margin: 0 0 0 ${btnSpacing};
+
+ [dir="rtl"] & {
+ margin: 0 ${btnSpacing} 0 0;
+ }
+ }
+ &:hover,
+ .buttonWrapper:hover & {
+ opacity: .5;
+ }
+
+ ${({ hideLabel }) => hideLabel && `
+ font-size: 0;
+ height: 0;
+ width: 0;
+ margin: 0 !important;
+ padding: 0 !important;
+ overflow: hidden;
+ display: none !important;
+ `}
+`;
+
+const ButtonWrapper = styled(BaseButton)`
+ border: none;
+ overflow: visible !important;
+ display: inline-block;
+ cursor: pointer;
+
+ &:focus,
+ &:hover {
+ outline: transparent;
+ outline-style: dotted;
+ outline-width: ${borderSize};
+ }
+
+ &:focus {
+ outline-style: solid;
+ }
+
+ &:-moz-focusring {
+ outline-color: transparent;
+ outline-offset: ${borderRadius};
+ }
+
+ &:active {
+ &:focus {
+ span:first-of-type::before {
+ border-radius: 50%;
+ outline: transparent;
+ outline-width: ${borderSize};
+ outline-style: solid;
+ }
+ }
+ }
+
+ line-height: 1.5;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ background: none;
+ padding: 0 !important;
+
+ &[aria-disabled="true"] > span {
+ cursor: not-allowed;
+ opacity: .65;
+ box-shadow: none;
+ }
+
+ & > span {
+ display: block;
+ text-align: center;
+ white-space: nowrap;
+ border: ${borderSizeSmall} solid transparent;
+ }
+
+ ${({ size }) => size === 'sm' && `
+ font-size: calc(${fontSizeSmall} * .85);
+ padding: ${smPaddingY} ${smPaddingX};
+
+ & > span {
+ border: ${borderSizeLarge} solid transparent;
+ }
+
+ & > label {
+ display: inline-block;
+ margin: 0 0 0 ${btnSpacing};
+
+ [dir="rtl"] & {
+ margin:0 ${btnSpacing} 0 0;
+ }
+ }
+ `}
+
+ ${({ size }) => size === 'md' && `
+ font-size: calc(${fontSizeBase} * .85);
+ padding: ${mdPaddingY} ${mdPaddingX};
+
+ & > span {
+ border: ${borderSizeLarge} solid transparent;
+ }
+ `}
+
+ ${({ size }) => size === 'lg' && `
+ font-size: ${fontSizeBase};
+ padding: ${lgPaddingY} ${lgPaddingX};
+ `}
+
+ ${({ size }) => size === 'jumbo' && `
+ font-size: 3rem;
+ padding: ${jumboPaddingY} ${jumboPaddingX};
+ `}
+
+ ${({ size, circle, color }) => size === 'lg' && circle && color === 'primary' && `
+ &:focus:not([aria-disabled="true"]){
+ & > span{
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnPrimaryBorder};
+ }
+ }
+
+ &:hover{
+ & > span{
+ filter: brightness(90%);
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryHoverBg} !important;
+ }
+ }
+
+ &:active:focus{
+ & > span{
+ filter: brightness(85%);
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryActiveBg};
+ }
+ }
+
+ &:active{
+ & > span{
+ filter: brightness(85%);
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryActiveBg};
+ }
+ }
+ `}
+
+ ${({
+ size, circle, ghost, color,
+ }) => size === 'lg' && circle && ghost && color === 'default' && `
+ span {
+ box-shadow: 0 0 1px 0px ${btnDefaultGhostColor} inset, 0 0 1px 0px ${btnDefaultGhostColor};
+ background-color: transparent !important;
+ border-color: ${btnDefaultGhostColor} !important;
+ }
+
+ & > span{
+ color: ${btnDefaultGhostColor};
+ }
+
+ &:focus:not([aria-disabled="true"]){
+ & > span{
+ background-color: ${btnDefaultGhostBg} !important;
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnDefaultGhostBorder};
+ border-color: transparent !important;
+ }
+ }
+
+ &:hover{
+ & > span{
+ filter: brightness(85%);
+ background-color: ${btnDefaultGhostBg} !important;
+ }
+ }
+
+ &:active:focus{
+ & > span{
+ filter: brightness(85%);
+ background-color: ${btnDefaultGhostActiveBg} !important;
+ }
+ }
+
+ &:active{
+ & > span{
+ filter: brightness(85%);
+ background-color: ${btnDefaultGhostActiveBg};
+ }
+ }
+ `}
+
+ ${({ ghost }) => ghost && `
+ & > span{
+ background-image: none;
+ background-color: transparent;
+ }
+ `}
+`;
+
+const ButtonSpan = styled.span`
+ border: none;
+ overflow: visible;
+ display: inline-block;
+ border-radius: ${borderSize};
+ font-weight: ${btnFontWeight};
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ cursor: pointer;
+ user-select: none;
+
+ &:-moz-focusring {
+ outline: none;
+ }
+
+ &:hover,
+ &:focus {
+ outline: transparent;
+ outline-style: dotted;
+ outline-width: ${borderSize};
+ text-decoration: none;
+ }
+
+ &:active,
+ &:focus {
+ outline: transparent;
+ outline-width: ${borderSize};
+ outline-style: solid;
+ }
+
+ &:active {
+ background-image: none;
+ }
+
+ &[aria-disabled="true"] {
+ cursor: not-allowed;
+ opacity: .65;
+ box-shadow: none;
+ }
+
+ &,
+ &:active {
+ &:focus {
+ span:first-of-type::before {
+ border-radius: ${borderSize};
+ }
+ }
+ }
+
+ ${({ size }) => size === 'sm' && `
+ font-size: calc(${fontSizeSmall} * .85);
+ padding: ${smPaddingY} ${smPaddingX};
+ `}
+
+ ${({ size }) => size === 'md' && `
+ font-size: calc(${fontSizeBase} * .85);
+ padding: ${mdPaddingY} ${mdPaddingX};
+ `}
+
+ ${({ size }) => size === 'lg' && `
+ height: 3rem;
+ width: 3rem;
+ display: flex !important;
+ align-items: center;
+ justify-content: center;
+ `}
+
+ ${({ size }) => size === 'jumbo' && `
+ font-size: 3rem;
+ padding: ${jumboPaddingY} ${jumboPaddingX};
+ `}
+
+ ${({ size, color }) => size === 'md' && color === 'light' && `
+ color: ${colorGray};
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${colorGray};
+ box-shadow: 0 0 0 1px #CDD6E0 !important;
+ background-color: #DCE4EC !important;
+ }
+
+ &:hover {
+ color: hsl(210, 13%, 20%) !important;
+ background-color: #DCE4EC !important;
+ }
+
+ &:active {
+ color: hsl(210, 13%, 20%) !important;
+ background-color: hsl(210, 30%, 80%) !important;
+ }
+
+ &:focus:hover {
+ color: hsl(210, 13%, 20%) !important;
+ box-shadow: 0 0 0 1px #CDD6E0 !important;
+ background-color: #DCE4EC !important;
+ }
+
+ &:focus:active {
+ color: hsl(210, 13%, 20%) !important;
+ box-shadow: 0 0 0 1px #CDD6E0 !important;
+ background-color: hsl(210, 30%, 80%) !important;
+ }
+ `}
+
+ ${({ size, color }) => size === 'md' && color === 'dark' && `
+ color: ${colorWhite};
+ background: none !important;
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${colorWhite};
+ box-shadow: 0 0 0 1px ${btnDefaultGhostBorder} !important;
+ background-color: ${btnDefaultGhostBg} !important;
+ }
+
+ &:hover {
+ color: hsl(0, 0%, 85%) !important;
+ background-color: ${btnDefaultGhostBg} !important;
+ }
+
+ &:active {
+ color: hsl(0, 0%, 85%) !important;
+ background-color: ${btnDefaultGhostActiveBg} !important;
+ }
+
+ &:focus:hover {
+ color: hsl(0, 0%, 85%) !important;
+ box-shadow: 0 0 0 1px ${btnDefaultGhostBorder} !important;
+ background-color: ${btnDefaultGhostBg} !important;
+ }
+
+ &:focus:active {
+ color: hsl(0, 0%, 85%) !important;
+ box-shadow: 0 0 0 1px ${btnDefaultGhostBorder} !important;
+ background-color: ${btnDefaultGhostActiveBg} !important;
+ }
+ `}
+
+ ${({ color, ghost }) => color === 'default' && !ghost && `
+ color: ${btnDefaultColor};
+ background-color: ${btnDefaultBg};
+ border: ${borderSizeLarge} solid transparent;
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnDefaultColor};
+ background-color: ${btnDefaultBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnPrimaryBg};
+ }
+
+ &:hover & {
+ color: ${btnDefaultBg};
+ }
+ `}
+
+ ${({ color }) => color === 'primary' && `
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryBg};
+ border: ${borderSizeLarge} solid transparent;
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnPrimaryBg};
+ }
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnPrimaryColor};
+ }
+
+ `}
+
+ ${({ color }) => color === 'success' && `
+ color: ${btnSuccessColor};
+ background-color: ${btnSuccessBg};
+ border: ${borderSizeLarge} solid transparent;
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnSuccessColor};
+ background-color: ${btnSuccessBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnSuccessBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnSuccessColor};
+ }
+ `}
+
+ ${({ color }) => color === 'warning' && `
+ color: ${btnWarningColor};
+ background-color: ${btnWarningBg};
+ border: ${borderSizeLarge} solid transparent;
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnWarningColor};
+ background-color: ${btnWarningBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnWarningBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnWarningColor};
+ }
+ `}
+
+ ${({ color }) => color === 'danger' && `
+ color: ${btnDangerColor};
+ background-color: ${btnDangerBg};
+ border: ${borderSizeLarge} solid transparent;
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnDangerColor};
+ background-color: ${btnDangerBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnDangerBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDangerColor};
+ }
+ `}
+
+ ${({ color }) => color === 'dark' && `
+ color: ${btnDarkColor};
+ background-color: ${btnDarkBg};
+ border: ${borderSizeLarge} solid transparent;
+
+ &:focus {
+ color: ${btnDarkColor};
+ background-color: ${btnDarkBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnDarkBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDarkColor};
+ }
+ `}
+
+ ${({ color }) => color === 'offline' && `
+ color: ${btnOfflineColor};
+ background-color: ${btnOfflineBg};
+ border: ${borderSizeLarge} solid transparent;
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnOfflineColor};
+ background-color: ${btnOfflineBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnOfflineBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnOfflineColor};
+ }
+ `}
+
+ ${({ color }) => color === 'muted' && `
+ color: ${btnMutedColor};
+ background-color: ${btnMutedBg};
+ border: ${borderSizeLarge} solid transparent;
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnMutedColor};
+ background-color: ${btnMutedBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnMutedBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnMutedColor};
+ }
+ `}
+
+ ${({ ghost, color, size }) => ghost && color === 'default' && size !== 'lg' && `
+ color: ${btnDefaultBg};
+ background-image: none;
+ background-color: transparent;
+ border: ${borderSizeLarge} solid transparent;
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnDefaultBg};
+ background-color: ${btnDefaultColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnDefaultBg} !important;
+ }
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDefaultBg};
+ background-color: ${btnDefaultColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'primary' && `
+ color: ${btnPrimaryBg};
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnPrimaryBg};
+ background-color: ${btnPrimaryColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnPrimaryBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnPrimaryBg};
+ background-color: ${btnPrimaryColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'success' && `
+ color: ${btnSuccessBg};
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnSuccessBg};
+ background-color: ${btnSuccessColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnSuccessBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnSuccessBg};
+ background-color: ${btnSuccessColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'warning' && `
+ color: ${btnWarningBg};
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnWarningBg};
+ background-color: ${btnWarningColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnWarningBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnWarningBg};
+ background-color: ${btnWarningColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'danger' && `
+ color: ${btnDangerBg};
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnDangerBg};
+ background-color: ${btnDangerColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnDangerBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDangerBg};
+ background-color: ${btnDangerColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'dark' && `
+ color: ${btnDarkBg};
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnDarkBg};
+ background-color: ${btnDarkColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnDarkBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDarkBg};
+ background-color: ${btnDarkColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'offline' && `
+ color: ${btnOfflineBg};
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnOfflineBg};
+ background-color: ${btnOfflineColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnOfflineBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnOfflineBg};
+ background-color: ${btnOfflineColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'muted' && `
+ color: ${btnMutedBg};
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnMutedBg};
+ background-color: ${btnMutedColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnMutedBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnMutedBg};
+ background-color: ${btnMutedColor};
+ }
+ `}
+
+ ${({ circle }) => circle && `
+ border-radius: 50%;
+ `}
+
+ ${({ circle, size }) => circle && size === 'sm' && `
+ padding: calc(${smPaddingX} / 2);
+ `}
+
+ ${({ circle, size }) => circle && size === 'md' && `
+ padding: calc(${mdPaddingX} / 2);
+ `}
+
+ ${({ circle, size }) => circle && size === 'lg' && `
+ padding: calc(${lgPaddingX} / 2);
+ `}
+
+ ${({ circle, size }) => circle && size === 'jumbo' && `
+ padding: calc(${jumboPaddingX} / 2);
+ `}
+
+ ${({ block }) => block && `
+ display: block;
+ width: 100%;
+ `}
+`;
+
+const Button = styled(BaseButton)`
+ border: ${borderSizeLarge} solid transparent;
+ border: none;
+ overflow: visible;
+ display: inline-block;
+ border-radius: ${borderSize};
+ font-weight: ${btnFontWeight};
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ cursor: pointer;
+ user-select: none;
+
+ &:-moz-focusring {
+ outline: none;
+ }
+
+ &:hover,
+ &:focus {
+ outline: transparent;
+ outline-style: dotted;
+ outline-width: ${borderSize};
+ text-decoration: none;
+ }
+
+ &:active,
+ &:focus {
+ outline: transparent;
+ outline-width: ${borderSize};
+ outline-style: solid;
+ }
+
+ &:active {
+ background-image: none;
+ }
+
+ &[aria-disabled="true"] {
+ cursor: not-allowed;
+ opacity: .65;
+ box-shadow: none;
+ }
+
+ &,
+ &:active {
+ &:focus {
+ span:first-of-type::before {
+ border-radius: ${borderSize};
+ }
+ }
+ }
+
+ ${({ size }) => size === 'sm' && `
+ font-size: calc(${fontSizeSmall} * .85);
+ padding: ${smPaddingY} ${smPaddingX};
+ `}
+
+ ${({ size }) => size === 'md' && `
+ font-size: calc(${fontSizeBase} * .85);
+ padding: ${mdPaddingY} ${mdPaddingX};
+ `}
+
+ ${({ size }) => size === 'lg' && `
+ font-size: calc(${fontSizeLarge} * .85);
+ padding: ${lgPaddingY} ${lgPaddingX};
+ `}
+
+ ${({ size }) => size === 'jumbo' && `
+ font-size: 3rem;
+ padding: ${jumboPaddingY} ${jumboPaddingX};
+ `}
+
+ ${({ color }) => color === 'default' && `
+ color: ${btnDefaultColor};
+ background-color: ${btnDefaultBg};
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnDefaultColor};
+ background-color: ${btnDefaultBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnPrimaryBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDefaultColor};
+ }
+ `}
+
+ ${({ color }) => color === 'primary' && `
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryBg};
+ border: ${borderSizeLarge} solid transparent !important;
+
+ &:focus:not([aria-disabled="true"]){
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnPrimaryBorder};
+ }
+
+ &:hover{
+ filter: brightness(90%);
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryHoverBg} !important;
+ }
+
+ &:active:focus{
+ filter: brightness(85%);
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryActiveBg};
+ }
+
+ &:active{
+ filter: brightness(85%);
+ color: ${btnPrimaryColor};
+ background-color: ${btnPrimaryActiveBg} !important;
+ }
+ `}
+
+ ${({ color }) => color === 'success' && `
+ color: ${btnSuccessColor};
+ background-color: ${btnSuccessBg};
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnSuccessColor};
+ background-color: ${btnSuccessBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnSuccessBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnSuccessColor};
+ }
+ `}
+
+ ${({ color }) => color === 'warning' && `
+ color: ${btnWarningColor};
+ background-color: ${btnWarningBg};
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnWarningColor};
+ background-color: ${btnWarningBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnWarningBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnWarningColor};
+ }
+ `}
+
+ ${({ color }) => color === 'danger' && `
+ color: ${btnDangerColor};
+ background-color: ${btnDangerBg};
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnDangerColor};
+ background-color: ${btnDangerBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnDangerBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDangerColor};
+ background-color: ${btnDangerBgHover};
+ }
+ `}
+
+ ${({ color }) => color === 'dark' && `
+ color: ${btnDarkColor};
+ background-color: ${btnDarkBg};
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnDarkColor};
+ background-color: ${btnDarkBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnDarkBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDarkColor};
+ }
+ `}
+
+ ${({ color }) => color === 'offline' && `
+ color: ${btnOfflineColor};
+ background-color: ${btnOfflineBg};
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnOfflineColor};
+ background-color: ${btnOfflineBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnOfflineBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnOfflineColor};
+ }
+ `}
+
+ ${({ color }) => color === 'muted' && `
+ color: ${btnMutedColor};
+ background-color: ${btnMutedBg};
+
+ &:focus,
+ .buttonWrapper:focus:not([aria-disabled="true"]) & {
+ color: ${btnMutedColor};
+ background-color: ${btnMutedBg};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSize} ${btnMutedBorder};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnMutedColor};
+ }
+ `}
+
+ ${({ color }) => color === 'secondary' && `
+ background: transparent;
+ color: ${colorGray};
+ border: 3px solid transparent;
+ border-radius: 4px;
+
+
+ &:focus {
+ background: hsl(210, 30%, 95%);
+ box-shadow: 0 0 0 ${borderSize} hsl(211, 87%, 80%);
+ }
+
+ &:hover {
+ background: hsl(210, 30%, 95%);
+ color: hsl(210, 13%, 35%);
+ }
+
+ &:active {
+ background: hsl(210, 30%, 89%);
+ color: hsl(210, 13%, 30%);
+ }
+
+ &:hover {
+ &:focus {
+ background: hsl(210, 30%, 95%);
+ color: hsl(210, 13%, 30%);
+ box-shadow: 0 0 0 ${borderSize} hsl(211, 87%, 80%);
+ }
+ }
+
+ &:focus {
+ &:active {
+ background: hsl(210, 30%, 89%);
+ color: hsl(210, 13%, 30%);
+ box-shadow: 0 0 0 ${borderSize} hsl(211, 87%, 80%);
+ }
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'default' && `
+ color: ${btnDefaultBg};
+ background-image: none;
+ background-color: transparent;
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnDefaultBg};
+ background-color: ${btnDefaultColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnDefaultBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDefaultBg};
+ background-color: ${btnDefaultColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'primary' && `
+ color: ${btnPrimaryBg};
+ background-image: none;
+ background-color: transparent;
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnPrimaryBg};
+ background-color: ${btnPrimaryColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnPrimaryBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnPrimaryBg};
+ background-color: ${btnPrimaryColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'success' && `
+ color: ${btnSuccessBg};
+ background-image: none;
+ background-color: transparent;
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnSuccessBg};
+ background-color: ${btnSuccessColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnSuccessBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnSuccessBg};
+ background-color: ${btnSuccessColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'warning' && `
+ color: ${btnWarningBg};
+ background-image: none;
+ background-color: transparent;
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnWarningBg};
+ background-color: ${btnWarningColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnWarningBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnWarningBg};
+ background-color: ${btnWarningColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'danger' && `
+ color: ${btnDangerBg};
+ background-image: none;
+ background-color: transparent;
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnDangerBg};
+ background-color: ${btnDangerColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnDangerBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDangerBg};
+ background-color: ${btnDangerColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'dark' && `
+ color: ${btnDarkBg};
+ background-image: none;
+ background-color: transparent;
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnDarkBg};
+ background-color: ${btnDarkColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnDarkBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnDarkBg};
+ background-color: ${btnDarkColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'offline' && `
+ color: ${btnOfflineBg};
+ background-image: none;
+ background-color: transparent;
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnOfflineBg};
+ background-color: ${btnOfflineColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnOfflineBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnOfflineBg};
+ background-color: ${btnOfflineColor};
+ }
+ `}
+
+ ${({ ghost, color }) => ghost && color === 'muted' && `
+ color: ${btnMutedBg};
+ background-image: none;
+ background-color: transparent;
+
+ &:focus,
+ .buttonWrapper:focus & {
+ color: ${btnMutedBg};
+ background-color: ${btnMutedColor};
+ background-clip: padding-box;
+ box-shadow: 0 0 0 ${borderSizeLarge} ${btnMutedBg};
+ }
+
+ &:hover,
+ .buttonWrapper:hover & {
+ color: ${btnMutedBg};
+ background-color: ${btnMutedColor};
+ }
+ `}
+
+ ${({ circle }) => circle && `
+ border-radius: 50%;
+ `}
+
+ ${({ circle, size }) => circle && size === 'sm' && `
+ padding: calc(${smPaddingX} / 2);
+ `}
+
+ ${({ circle, size }) => circle && size === 'md' && `
+ padding: calc(${mdPaddingX} / 2);
+ `}
+
+ ${({ circle, size }) => circle && size === 'lg' && `
+ padding: calc(${lgPaddingX} / 2);
+ `}
+
+ ${({ circle, size }) => circle && size === 'jumbo' && `
+ padding: calc(${jumboPaddingX} / 2);
+ `}
+
+ ${({ block }) => block && `
+ display: block;
+ width: 100%;
+ `}
+
+ ${({ iconRight }) => iconRight && `
+ display: flex;
+ flex-direction: row-reverse;
+ `}
+`;
+
+export default {
+ ButtonIcon,
+ EmojiButtonSibling,
+ ButtonLabel,
+ ButtonWrapper,
+ ButtonSpan,
+ Button,
+};
diff --git a/src/2.7.12/imports/ui/components/common/checkbox/base.jsx b/src/2.7.12/imports/ui/components/common/checkbox/base.jsx
new file mode 100644
index 00000000..8d08c4fb
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/checkbox/base.jsx
@@ -0,0 +1,66 @@
+import React, { createRef, PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { findDOMNode } from 'react-dom';
+
+const propTypes = {
+ disabled: PropTypes.bool,
+ checked: PropTypes.bool,
+ onChange: PropTypes.func.isRequired,
+ ariaLabelledBy: PropTypes.string,
+ ariaLabel: PropTypes.string,
+ ariaDescribedBy: PropTypes.string,
+ ariaDesc: PropTypes.string,
+};
+
+const defaultProps = {
+ disabled: false,
+ checked: false,
+ ariaLabelledBy: null,
+ ariaLabel: null,
+ ariaDescribedBy: null,
+ ariaDesc: null,
+};
+
+export default class Base extends PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.onChange = props.onChange;
+ this.handleChange = this.handleChange.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+
+ this.element = createRef();
+ }
+
+ componentDidMount() {
+ const element = findDOMNode(this.element.current);
+ if (element) element.addEventListener('keydown', this.handleKeyDown);
+ }
+
+ componentWillUnmount() {
+ const element = findDOMNode(this.element.current);
+ if (element) element.removeEventListener('keydown', this.handleKeyDown);
+ }
+
+ handleKeyDown(event) {
+ const { key } = event;
+ const node = findDOMNode(this.element.current);
+ if (key === 'Enter' && node) {
+ const input = node.getElementsByTagName('input')[0];
+ input?.click();
+ }
+ }
+
+ handleChange() {
+ const { disabled, keyValue } = this.props;
+ if (disabled) return;
+ this.onChange(keyValue);
+ }
+
+ render() {
+ return null;
+ }
+}
+
+Base.propTypes = propTypes;
+Base.defaultProps = defaultProps;
diff --git a/src/2.7.12/imports/ui/components/common/checkbox/component.jsx b/src/2.7.12/imports/ui/components/common/checkbox/component.jsx
new file mode 100644
index 00000000..057ebb45
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/checkbox/component.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import Base from './base';
+import Styled from './styles';
+
+export default class Checkbox extends Base {
+ render() {
+ const {
+ ariaLabel, ariaDesc, ariaDescribedBy, ariaLabelledBy, checked, disabled, label,
+ } = this.props;
+
+ const checkbox = (
+
+ );
+
+ return (
+ <>
+ {label ? (
+
+ ) : checkbox}
+
{ariaDesc}
+ >
+ );
+ }
+}
diff --git a/src/2.7.12/imports/ui/components/common/checkbox/styles.js b/src/2.7.12/imports/ui/components/common/checkbox/styles.js
new file mode 100644
index 00000000..4cc5312e
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/checkbox/styles.js
@@ -0,0 +1,25 @@
+import { colorText, colorSuccess } from '/imports/ui/stylesheets/styled-components/palette';
+import BaseCheckbox from '@mui/material/Checkbox';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import { styled } from '@mui/system';
+
+const Checkbox = styled(BaseCheckbox)(() => ({
+ '&.Mui-checked': {
+ color: `${colorSuccess} !important`,
+ },
+}));
+
+const Label = styled(FormControlLabel)(() => ({
+ '& .MuiFormControlLabel-label': {
+ fontFamily: 'inherit !important',
+ color: `${colorText} !important`,
+ },
+ '&.Mui-disabled': {
+ cursor: 'not-allowed !important',
+ },
+}));
+
+export default {
+ Checkbox,
+ Label,
+};
diff --git a/src/2.7.12/imports/ui/components/common/control-header/component.jsx b/src/2.7.12/imports/ui/components/common/control-header/component.jsx
new file mode 100644
index 00000000..2b3bcf2d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/control-header/component.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Styled from './styles';
+import Left from './left/component';
+import Right from './right/component';
+
+const Header = ({
+ leftButtonProps,
+ rightButtonProps,
+ customRightButton,
+ 'data-test': dataTest,
+ ...rest
+}) => {
+ const renderCloseButton = () => (
+
+ );
+
+ const renderCustomRightButton = () => (
+
+ {customRightButton}
+
+ );
+
+ return (
+
+ {leftButtonProps ? :
}
+ {customRightButton
+ ? renderCustomRightButton()
+ : rightButtonProps
+ ? renderCloseButton()
+ : null}
+
+ );
+}
+
+Header.propTypes = {
+ leftButtonProps: PropTypes.object,
+ rightButtonProps: PropTypes.object,
+ customRightButton: PropTypes.element,
+ dataTest: PropTypes.string,
+};
+
+export default Header;
diff --git a/src/2.7.12/imports/ui/components/common/control-header/left/component.jsx b/src/2.7.12/imports/ui/components/common/control-header/left/component.jsx
new file mode 100644
index 00000000..818ac655
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/control-header/left/component.jsx
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Styled from './styles';
+
+class Left extends Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+Left.propTypes = {
+ accessKey: PropTypes.any,
+ 'aria-label': PropTypes.string,
+ 'data-test': PropTypes.string,
+ label: PropTypes.string,
+ onClick: PropTypes.func,
+};
+
+export default Left;
diff --git a/src/2.7.12/imports/ui/components/common/control-header/left/styles.js b/src/2.7.12/imports/ui/components/common/control-header/left/styles.js
new file mode 100644
index 00000000..9b227bc4
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/control-header/left/styles.js
@@ -0,0 +1,31 @@
+import styled from 'styled-components';
+import Button from '/imports/ui/components/common/button/component';
+import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
+
+const HideButton = styled(Button)`
+ padding: 0;
+ margin: 0;
+ line-height: normal;
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ & > i,
+ & > i::before {
+ width: auto;
+ font-size: ${fontSizeBase} !important;
+
+ [dir="rtl"] & {
+ -webkit-transform: scale(-1, 1);
+ -moz-transform: scale(-1, 1);
+ -ms-transform: scale(-1, 1);
+ -o-transform: scale(-1, 1);
+ transform: scale(-1, 1);
+ }
+ }
+`;
+
+export default { HideButton };
diff --git a/src/2.7.12/imports/ui/components/common/control-header/right/component.jsx b/src/2.7.12/imports/ui/components/common/control-header/right/component.jsx
new file mode 100644
index 00000000..f0cff6b9
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/control-header/right/component.jsx
@@ -0,0 +1,32 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Styled from './styles';
+
+class Right extends Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+Right.propTypes = {
+ accessKey: PropTypes.any,
+ 'aria-label': PropTypes.string,
+ 'data-test': PropTypes.string,
+ icon: PropTypes.string,
+ label: PropTypes.string,
+ onClick: PropTypes.func,
+};
+
+export default Right;
diff --git a/src/2.7.12/imports/ui/components/common/control-header/right/styles.js b/src/2.7.12/imports/ui/components/common/control-header/right/styles.js
new file mode 100644
index 00000000..63342cca
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/control-header/right/styles.js
@@ -0,0 +1,18 @@
+import styled from 'styled-components';
+import Button from '/imports/ui/components/common/button/component';
+import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
+
+const CloseButton = styled(Button)`
+ span:first-of-type {
+ padding: 0;
+ margin: 0;
+
+ & > i,
+ & > i::before {
+ width: auto;
+ font-size: ${fontSizeBase};
+ }
+ }
+`;
+
+export default { CloseButton };
diff --git a/src/2.7.12/imports/ui/components/common/control-header/styles.js b/src/2.7.12/imports/ui/components/common/control-header/styles.js
new file mode 100644
index 00000000..fa65691e
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/control-header/styles.js
@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+import { jumboPaddingY } from '/imports/ui/stylesheets/styled-components/general';
+
+const Header = styled.header`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: ${jumboPaddingY};
+`;
+
+const RightWrapper = styled.div`
+ & > div {
+ display: flex;
+ }
+`;
+
+export default {
+ Header,
+ RightWrapper,
+};
diff --git a/src/2.7.12/imports/ui/components/common/error-boundary/component.jsx b/src/2.7.12/imports/ui/components/common/error-boundary/component.jsx
new file mode 100644
index 00000000..a425935b
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/error-boundary/component.jsx
@@ -0,0 +1,43 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import logger from '/imports/startup/client/logger';
+
+const propTypes = {
+ children: PropTypes.element.isRequired,
+ Fallback: PropTypes.func.isRequired,
+};
+
+class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { error: false, errorInfo: null };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ this.setState({
+ error,
+ errorInfo,
+ });
+ logger.error({
+ logCode: 'Error_Boundary_wrapper',
+ extraInfo: { error, errorInfo },
+ }, 'generic error boundary logger');
+ }
+
+ render() {
+ const { error } = this.state;
+ const { children, Fallback } = this.props;
+
+ return (error ? ( ) : children);
+ }
+}
+
+ErrorBoundary.propTypes = propTypes;
+
+export const withErrorBoundary = (WrappedComponent, FallbackComponent) => (props) => (
+
+
+
+);
+
+export default ErrorBoundary;
diff --git a/src/2.7.12/imports/ui/components/common/fallback-errors/fallback-modal/component.jsx b/src/2.7.12/imports/ui/components/common/fallback-errors/fallback-modal/component.jsx
new file mode 100644
index 00000000..e92c8246
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/fallback-errors/fallback-modal/component.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import ModalSimple from '/imports/ui/components/common/modal/simple/component';
+import { defineMessages, injectIntl } from 'react-intl';
+import FallbackView from '../fallback-view/component';
+
+const intlMessages = defineMessages({
+ ariaTitle: {
+ id: 'app.error.fallback.modal.ariaTitle',
+ description: 'title announced when fallback modal is showed',
+ },
+});
+
+const FallbackModal = ({ error, intl }) => {
+ return (
+
+
+
+)};
+
+export default injectIntl(FallbackModal);
diff --git a/src/2.7.12/imports/ui/components/common/fallback-errors/fallback-view/component.jsx b/src/2.7.12/imports/ui/components/common/fallback-errors/fallback-view/component.jsx
new file mode 100644
index 00000000..030a8ee8
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/fallback-errors/fallback-view/component.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import Styled from './styles';
+
+const intlMessages = defineMessages({
+ title: {
+ id: 'app.error.fallback.presentation.title',
+ description: 'title for presentation when fallback is showed',
+ },
+ description: {
+ id: 'app.error.fallback.presentation.description',
+ description: 'description for presentation when fallback is showed',
+ },
+ reloadButton: {
+ id: 'app.error.fallback.presentation.reloadButton',
+ description: 'Button label when fallback is showed',
+ },
+});
+
+const propTypes = {
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func.isRequired,
+ }).isRequired,
+ error: PropTypes.shape({
+ message: PropTypes.string.isRequired,
+ }),
+};
+
+const defaultProps = {
+ error: {
+ message: '',
+ },
+};
+
+const FallbackView = ({ error, intl }) => (
+
+
+ {intl.formatMessage(intlMessages.title)}
+
+
+ {intl.formatMessage(intlMessages.description)}
+
+
+
+ {error.message}
+
+
+ window.location.reload()}
+ label={intl.formatMessage(intlMessages.reloadButton)}
+ />
+
+
+);
+
+FallbackView.propTypes = propTypes;
+FallbackView.defaultProps = defaultProps;
+
+export default injectIntl(FallbackView);
diff --git a/src/2.7.12/imports/ui/components/common/fallback-errors/fallback-view/styles.js b/src/2.7.12/imports/ui/components/common/fallback-errors/fallback-view/styles.js
new file mode 100644
index 00000000..4478b333
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/fallback-errors/fallback-view/styles.js
@@ -0,0 +1,63 @@
+import styled from 'styled-components';
+import {
+ colorWhite,
+ colorBackground,
+ colorGrayLighter,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
+import Button from '/imports/ui/components/common/button/component';
+
+const Background = styled.div`
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ background-color: ${colorBackground};
+ color: ${colorWhite};
+ text-align: center;
+`;
+
+const CodeError = styled.h1`
+ margin: 0;
+ font-size: 3rem;
+ color: ${colorWhite};
+`;
+
+const Message = styled.h1`
+ margin: 0;
+ color: ${colorWhite};
+ font-size: 1.25rem;
+ font-weight: 400;
+`;
+
+const Separator = styled.div`
+ height: 0;
+ width: 5rem;
+ border: 1px solid ${colorGrayLighter};
+ margin: 1.5rem 0 1.5rem 0;
+ align-self: center;
+ opacity: .75;
+`;
+
+const SessionMessage = styled.div`
+ margin: 0;
+ color: ${colorWhite};
+ font-size: ${fontSizeBase};
+ font-weight: 400;
+ margin-bottom: 1.5rem;
+`;
+
+const ReloadButton = styled(Button)`
+ min-width: 9rem;
+ height: 2rem;
+`;
+
+export default {
+ Background,
+ CodeError,
+ Message,
+ Separator,
+ SessionMessage,
+ ReloadButton,
+};
diff --git a/src/2.7.12/imports/ui/components/common/file-reader/component.jsx b/src/2.7.12/imports/ui/components/common/file-reader/component.jsx
new file mode 100644
index 00000000..f7a48eeb
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/file-reader/component.jsx
@@ -0,0 +1,179 @@
+import React, { useRef } from 'react';
+import { defineMessages } from 'react-intl';
+import { toast } from 'react-toastify';
+import { notify } from '/imports/ui/services/notification';
+import Icon from '/imports/ui/components/common/icon/component';
+import Styled from './styles';
+import { uniqueId } from '/imports/utils/string-utils';
+
+const intlMessages = defineMessages({
+ maximumSizeExceeded: {
+ id: 'app.video.virtualBackground.maximumFileSizeExceeded',
+ description: 'Label for the maximum file size exceeded notification',
+ },
+ typeNotAllowed: {
+ id: 'app.video.virtualBackground.typeNotAllowed',
+ description: 'Label for the file type not allowed notification',
+ },
+ errorOnRead: {
+ id: 'app.video.virtualBackground.errorOnRead',
+ description: 'Label for the error on read notification',
+ },
+ uploaded: {
+ id: 'app.video.virtualBackground.uploaded',
+ description: 'Label for when the file is uploaded',
+ },
+ uploading: {
+ id: 'app.video.virtualBackground.uploading',
+ description: 'Label for when the file is uploading',
+ },
+});
+
+const STATUS = {
+ LOADING: 'loading',
+ DONE: 'done',
+ ERROR: 'error',
+};
+
+/**
+ * HOC for injecting a file reader utility.
+ * @param {React.Component} Component
+ * @param {string[]} mimeTypesAllowed String array containing MIME types allowed.
+ * @param {number} maxFileSize Max file size allowed in Mbytes.
+ * @returns A new component which accepts the same props as the wrapped component plus
+ * a function called readFile.
+ */
+const withFileReader = (
+ Component,
+ mimeTypesAllowed,
+ maxFileSize,
+) => (props) => {
+ const { intl } = props;
+ const toastId = useRef(null);
+
+ const parseFilename = (filename = '') => {
+ const substrings = filename.split('.');
+ substrings.pop();
+ const filenameWithoutExtension = substrings.join('');
+ return filenameWithoutExtension;
+ };
+
+ const renderToastContent = (text, status) => {
+ let icon;
+ let statusMessage;
+
+ switch (status) {
+ case STATUS.LOADING:
+ icon = 'blank';
+ statusMessage = intl.formatMessage(intlMessages.uploading);
+ break;
+ case STATUS.DONE:
+ icon = 'check';
+ statusMessage = intl.formatMessage(intlMessages.uploaded);
+ break;
+ case STATUS.ERROR:
+ default:
+ icon = 'circle_close'
+ statusMessage = intl.formatMessage(intlMessages.errorOnRead);
+ }
+
+ return (
+
+
+
+ {text}
+
+
+
+
+
+
+ {statusMessage}
+
+
+
+ );
+ };
+
+ const renderToast = (text = '', status = STATUS.DONE, callback) => {
+ if (toastId.current) {
+ toast.dismiss(toastId.current);
+ }
+
+ toastId.current = toast.info(renderToastContent(text, status), {
+ hideProgressBar: status === STATUS.DONE ? false : true,
+ autoClose: status === STATUS.DONE ? 5000 : false,
+ newestOnTop: true,
+ closeOnClick: true,
+ onClose: () => {
+ toastId.current = null;
+ },
+ onOpen: () => {
+ if (typeof callback === 'function') callback();
+ },
+ });
+ };
+
+ const readFile = (
+ file,
+ onSuccess = () => {},
+ onError = () => {},
+ ) => {
+ if (!file) return;
+
+ const { name, size, type } = file;
+ const sizeInKB = size / 1024;
+
+ if (sizeInKB > maxFileSize) {
+ notify(
+ intl.formatMessage(
+ intlMessages.maximumSizeExceeded,
+ { 0: (maxFileSize / 1000).toFixed(0) },
+ ),
+ 'error',
+ );
+ return onError(new Error('Maximum file size exceeded.'));
+ }
+
+ if (!mimeTypesAllowed.includes(type)) {
+ notify(
+ intl.formatMessage(intlMessages.typeNotAllowed),
+ 'error',
+ );
+ return onError(new Error('File type not allowed.'));
+ }
+
+ const filenameWithoutExtension = parseFilename(name);
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ const data = {
+ filename: filenameWithoutExtension,
+ data: e.target.result,
+ uniqueId: uniqueId(),
+ };
+ renderToast(name, STATUS.DONE, () => { onSuccess(data); });
+ };
+
+ reader.onerror = () => {
+ renderToast(name, STATUS.ERROR, () => {
+ onError(new Error('Something went wrong when reading the file.'));
+ });
+ };
+
+ reader.onloadstart = () => {
+ renderToast(name, STATUS.LOADING);
+ };
+
+ reader.readAsDataURL(file);
+ };
+
+ return ;
+}
+
+export default withFileReader;
diff --git a/src/2.7.12/imports/ui/components/common/file-reader/styles.js b/src/2.7.12/imports/ui/components/common/file-reader/styles.js
new file mode 100644
index 00000000..6ddf3475
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/file-reader/styles.js
@@ -0,0 +1,93 @@
+import styled, { css, keyframes } from 'styled-components';
+import Icon from '/imports/ui/components/common/icon/component';
+import {
+ colorDanger,
+ colorGray,
+ colorGrayLightest,
+ colorSuccess,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import {
+ fileLineWidth,
+ iconPaddingMd,
+ mdPaddingY,
+ statusIconSize,
+} from '/imports/ui/stylesheets/styled-components/general';
+import {
+ headingsFontWeight,
+} from '/imports/ui/stylesheets/styled-components/typography';
+
+const rotate = keyframes`
+ 0% { transform: rotate(0); }
+ 100% { transform: rotate(360deg); }
+`;
+
+const ToastIcon = styled(Icon)`
+ font-size: 117%;
+ width: ${statusIconSize};
+ height: ${statusIconSize};
+ position: relative;
+ left: 8px;
+
+ [dir="rtl"] & {
+ left: unset;
+ right: 8px;
+ }
+
+ ${({ done }) => done && `
+ color: ${colorSuccess};
+ `}
+
+ ${({ error }) => error && `
+ color: ${colorDanger};
+ `}
+
+ ${({ loading }) => loading && css`
+ color: ${colorGrayLightest};
+ border: 1px solid;
+ border-radius: 50%;
+ border-right-color: ${colorGray};
+ animation: ${rotate} 1s linear infinite;
+ `}
+`;
+
+const FileLine = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: ${fileLineWidth};
+ padding-bottom: ${iconPaddingMd};
+`;
+
+const ToastFileName = styled.span`
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ margin-left: ${mdPaddingY};
+ width: auto;
+ text-align: left;
+ font-weight: ${headingsFontWeight};
+
+ [dir="rtl"] & {
+ margin-right: ${mdPaddingY};
+ margin-left: 0;
+ text-align: right;
+ }
+`;
+
+const Content = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+`;
+
+const Status = styled.span`
+ font-size: 70%;
+`;
+
+export default {
+ ToastIcon,
+ FileLine,
+ ToastFileName,
+ Content,
+ Status,
+};
diff --git a/src/2.7.12/imports/ui/components/common/fullscreen-button/component.jsx b/src/2.7.12/imports/ui/components/common/fullscreen-button/component.jsx
new file mode 100644
index 00000000..22c8af77
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/fullscreen-button/component.jsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import Styled from './styles';
+import { ACTIONS } from '/imports/ui/components/layout/enums';
+
+const intlMessages = defineMessages({
+ fullscreenButton: {
+ id: 'app.fullscreenButton.label',
+ description: 'Fullscreen label',
+ },
+ fullscreenUndoButton: {
+ id: 'app.fullscreenUndoButton.label',
+ description: 'Undo fullscreen label',
+ },
+});
+
+const propTypes = {
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func.isRequired,
+ }).isRequired,
+ fullscreenRef: PropTypes.instanceOf(Element),
+ dark: PropTypes.bool,
+ bottom: PropTypes.bool,
+ isIphone: PropTypes.bool,
+ isFullscreen: PropTypes.bool,
+ elementName: PropTypes.string,
+ handleToggleFullScreen: PropTypes.func.isRequired,
+ color: PropTypes.string,
+ fullScreenStyle: PropTypes.bool,
+};
+
+const defaultProps = {
+ dark: false,
+ bottom: false,
+ isIphone: false,
+ isFullscreen: false,
+ elementName: '',
+ color: 'default',
+ fullScreenStyle: true,
+ fullscreenRef: null,
+};
+
+const FullscreenButtonComponent = ({
+ intl,
+ dark,
+ bottom,
+ elementName,
+ elementId,
+ elementGroup,
+ isIphone,
+ isFullscreen,
+ layoutContextDispatch,
+ currentElement,
+ currentGroup,
+ color,
+ fullScreenStyle,
+ fullscreenRef,
+ handleToggleFullScreen,
+}) => {
+ if (isIphone) return null;
+
+ const formattedLabel = (fullscreen) => (fullscreen
+ ? intl.formatMessage(
+ intlMessages.fullscreenUndoButton,
+ ({ 0: elementName || '' }),
+ )
+ : intl.formatMessage(
+ intlMessages.fullscreenButton,
+ ({ 0: elementName || '' }),
+ )
+ );
+
+ const handleClick = () => {
+ handleToggleFullScreen(fullscreenRef);
+ const newElement = (elementId === currentElement) ? '' : elementId;
+ const newGroup = (elementGroup === currentGroup) ? '' : elementGroup;
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_FULLSCREEN_ELEMENT,
+ value: {
+ element: newElement,
+ group: newGroup,
+ },
+ });
+ };
+
+ return (
+
+ handleClick()}
+ label={formattedLabel(isFullscreen)}
+ hideLabel
+ isStyled={fullScreenStyle}
+ data-test="webcamFullscreenButton"
+ />
+
+ );
+};
+
+FullscreenButtonComponent.propTypes = propTypes;
+FullscreenButtonComponent.defaultProps = defaultProps;
+
+export default injectIntl(FullscreenButtonComponent);
diff --git a/src/2.7.12/imports/ui/components/common/fullscreen-button/container.jsx b/src/2.7.12/imports/ui/components/common/fullscreen-button/container.jsx
new file mode 100644
index 00000000..f4f7d620
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/fullscreen-button/container.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import FullscreenButtonComponent from './component';
+import { layoutSelect, layoutDispatch } from '/imports/ui/components/layout/context';
+import FullscreenService from './service';
+
+const FullscreenButtonContainer = (props) => ;
+
+export default (props) => {
+ const handleToggleFullScreen = (ref) => FullscreenService.toggleFullScreen(ref);
+ const { isFullscreen } = props;
+
+ const isIphone = !!(navigator.userAgent.match(/iPhone/i));
+
+ const fullscreen = layoutSelect((i) => i.fullscreen);
+ const { element: currentElement, group: currentGroup } = fullscreen;
+ const layoutContextDispatch = layoutDispatch();
+
+ return (
+
+ );
+};
diff --git a/src/2.7.12/imports/ui/components/common/fullscreen-button/service.js b/src/2.7.12/imports/ui/components/common/fullscreen-button/service.js
new file mode 100644
index 00000000..550df065
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/fullscreen-button/service.js
@@ -0,0 +1,56 @@
+function getFullscreenElement() {
+ if (document.fullscreenElement) return document.fullscreenElement;
+ if (document.webkitFullscreenElement) return document.webkitFullscreenElement;
+ if (document.mozFullScreenElement) return document.mozFullScreenElement;
+ if (document.msFullscreenElement) return document.msFullscreenElement;
+ return null;
+}
+
+const isFullScreen = (element) => {
+ if (getFullscreenElement() && getFullscreenElement() === element) {
+ return true;
+ }
+ return false;
+};
+
+function cancelFullScreen() {
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ } else if (document.mozCancelFullScreen) {
+ document.mozCancelFullScreen();
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen();
+ }
+}
+
+function fullscreenRequest(element) {
+ if (element.requestFullscreen) {
+ element.requestFullscreen();
+ } else if (element.mozRequestFullScreen) {
+ element.mozRequestFullScreen();
+ } else if (element.webkitRequestFullscreen) {
+ element.webkitRequestFullscreen();
+ } else if (element.msRequestFullscreen) {
+ element.msRequestFullscreen();
+ } else {
+ return;
+ }
+ document.activeElement.blur();
+ element.focus();
+}
+
+const toggleFullScreen = (ref = null) => {
+ const element = ref || document.documentElement;
+
+ if (isFullScreen(element)) {
+ cancelFullScreen();
+ } else {
+ fullscreenRequest(element);
+ }
+};
+
+export default {
+ toggleFullScreen,
+ isFullScreen,
+ getFullscreenElement,
+};
diff --git a/src/2.7.12/imports/ui/components/common/fullscreen-button/styles.js b/src/2.7.12/imports/ui/components/common/fullscreen-button/styles.js
new file mode 100644
index 00000000..2e7300fb
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/fullscreen-button/styles.js
@@ -0,0 +1,82 @@
+import styled from 'styled-components';
+import Button from '/imports/ui/components/common/button/component';
+import {
+ colorTransparent,
+ colorWhite,
+ colorBlack,
+} from '/imports/ui/stylesheets/styled-components/palette';
+
+const FullscreenButtonWrapper = styled.div`
+ position: absolute;
+ right: 0;
+ left: auto;
+ background-color: ${colorTransparent};
+ cursor: pointer;
+ border: 0;
+ z-index: 2;
+ margin: 2px;
+
+ [dir="rtl"] & {
+ right: auto;
+ left :0;
+ }
+
+ [class*="presentationZoomControls"] & {
+ position: relative !important;
+ }
+
+ ${({ theme }) => theme === 'dark' && `
+ background-color: rgba(0,0,0,.3);
+
+ & button i {
+ color: ${colorWhite};
+ }
+ `}
+
+ ${({ theme }) => theme === 'light' && `
+ background-color: ${colorTransparent};
+
+ & button i {
+ color: ${colorBlack};
+ }
+ `}
+
+ ${({ position }) => position === 'bottom' && `
+ bottom: 0;
+ `}
+
+ ${({ position }) => position === 'top' && `
+ top: 0;
+ `}
+`;
+
+const FullscreenButton = styled(Button)`
+ ${({ isStyled }) => isStyled && `
+ &,
+ &:active,
+ &:hover,
+ &:focus {
+ background-color: ${colorTransparent} !important;
+ border: none !important;
+
+ i {
+ border: none !important;
+ background-color: ${colorTransparent} !important;
+ }
+ }
+ padding: 5px;
+
+ &:hover {
+ border: 0;
+ }
+
+ i {
+ font-size: 1rem;
+ }
+ `}
+`;
+
+export default {
+ FullscreenButtonWrapper,
+ FullscreenButton,
+};
diff --git a/src/2.7.12/imports/ui/components/common/icon/component.jsx b/src/2.7.12/imports/ui/components/common/icon/component.jsx
new file mode 100644
index 00000000..8199fb5d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/icon/component.jsx
@@ -0,0 +1,36 @@
+import React, { memo } from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import { omit } from 'radash';
+import Styled from './styles';
+
+const propTypes = {
+ iconName: PropTypes.string.isRequired,
+ prependIconName: PropTypes.string,
+ rotate: PropTypes.bool,
+};
+
+const defaultProps = {
+ prependIconName: 'icon-bbb-',
+ rotate: false,
+};
+
+const Icon = ({
+ className,
+ prependIconName,
+ iconName,
+ rotate,
+ ...props
+}) => (
+
+);
+
+export default memo(Icon);
+
+Icon.propTypes = propTypes;
+Icon.defaultProps = defaultProps;
diff --git a/src/2.7.12/imports/ui/components/common/icon/styles.js b/src/2.7.12/imports/ui/components/common/icon/styles.js
new file mode 100644
index 00000000..6f5e2ca5
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/icon/styles.js
@@ -0,0 +1,12 @@
+import styled from 'styled-components';
+
+const Icon = styled.i`
+ ${({ $rotate }) => $rotate && `
+ transform: rotate(180deg);
+ margin-top: 20%;
+ `}
+`;
+
+export default {
+ Icon,
+};
diff --git a/src/2.7.12/imports/ui/components/common/loading-screen/component.jsx b/src/2.7.12/imports/ui/components/common/loading-screen/component.jsx
new file mode 100644
index 00000000..83e827ca
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/loading-screen/component.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import Styled from './styles';
+import Settings from '/imports/ui/services/settings';
+
+const { animations } = Settings.application;
+
+const LoadingScreen = ({ children }) => (
+
+
+
+
+
+
+
+ {children}
+
+
+);
+
+export default LoadingScreen;
diff --git a/src/2.7.12/imports/ui/components/common/loading-screen/styles.js b/src/2.7.12/imports/ui/components/common/loading-screen/styles.js
new file mode 100644
index 00000000..b0b40dba
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/loading-screen/styles.js
@@ -0,0 +1,72 @@
+import styled, { css, keyframes } from 'styled-components';
+import { mdPaddingX } from '/imports/ui/stylesheets/styled-components/general';
+import { loaderBg, loaderBullet, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
+import { fontSizeLarge } from '/imports/ui/stylesheets/styled-components/typography';
+
+const Background = styled.div`
+ position: fixed;
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ background-color: ${loaderBg};
+ z-index: 4;
+`;
+
+const skBouncedelay = keyframes`
+ 0%,
+ 80%,
+ 100% {
+ transform: scale(0);
+ }
+ 40% {
+ transform: scale(1.0);
+ }
+`;
+
+const Spinner = styled.div`
+ width: 100%;
+ text-align: center;
+ height: 22px;
+ margin-bottom: ${mdPaddingX};
+
+ & > div {
+ width: 18px;
+ height: 18px;
+ margin: 0 5px;
+ background-color: ${loaderBullet};
+ border-radius: 100%;
+ display: inline-block;
+
+ ${({ animations }) => animations && css`
+ animation: ${skBouncedelay} 1.4s infinite ease-in-out both;
+ `}
+ }
+`;
+
+const Bounce1 = styled.div`
+ ${({ animations }) => animations && `
+ animation-delay: -0.32s !important;
+ `}
+`;
+
+const Bounce2 = styled.div`
+ ${({ animations }) => animations && `
+ animation-delay: -0.16s !important;
+ `}
+`;
+
+const Message = styled.div`
+ font-size: ${fontSizeLarge};
+ color: ${colorWhite};
+ text-align: center;
+`;
+
+export default {
+ Background,
+ Spinner,
+ Bounce1,
+ Bounce2,
+ Message,
+};
diff --git a/src/2.7.12/imports/ui/components/common/locales-dropdown/component.jsx b/src/2.7.12/imports/ui/components/common/locales-dropdown/component.jsx
new file mode 100644
index 00000000..46fbca02
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/locales-dropdown/component.jsx
@@ -0,0 +1,86 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl } from 'react-intl';
+
+const DEFAULT_VALUE = 'select';
+const DEFAULT_KEY = -1;
+
+const propTypes = {
+ allLocales: PropTypes.arrayOf(PropTypes.object).isRequired,
+ value: PropTypes.string,
+ handleChange: PropTypes.func.isRequired,
+ elementId: PropTypes.string.isRequired,
+ selectMessage: PropTypes.string.isRequired,
+};
+
+const defaultProps = {
+ value: null,
+};
+
+class LocalesDropdown extends PureComponent {
+ // returns an array with the base language list + variations of currently selected language
+ filterLocaleVariations(value) {
+ const { allLocales } = this.props;
+ if (allLocales) {
+ if (Meteor.settings.public.app.showAllAvailableLocales) {
+ return allLocales;
+ }
+
+ // splits value if not empty
+ const splitValue = (value) ? value.split('-')[0] : '';
+
+ const allLocaleCodes = [];
+ allLocales.map((item) => allLocaleCodes.push(item.locale));
+
+ /*
+ locales show if:
+ 1. it is a general version of a locale with no specific locales
+ 2. it is a specific version of a selected locale with many specific versions
+ 3. it is a specific version of a locale with no general locale
+ */
+ return allLocales.filter(
+ (locale) => (!locale.locale.includes('-') || locale.locale.split('-')[0] === splitValue || !allLocaleCodes.includes(locale.locale.split('-')[0])),
+ );
+ }
+ return [];
+ }
+
+ render() {
+ const {
+ value, handleChange, elementId, selectMessage, ariaLabel, intl,
+ } = this.props;
+ const defaultLocale = value || DEFAULT_VALUE;
+
+ const availableLocales = this.filterLocaleVariations(value);
+
+ return (
+
+
+ {selectMessage}
+
+ {availableLocales.map((localeItem) => {
+ const localizedName = localeItem.locale !== value && intl.formatMessage({
+ id: `app.submenu.application.localeDropdown.${localeItem.locale}`,
+ defaultMessage: ``,
+ });
+
+ return (
+
+ {localeItem.name}{localizedName && ` - ${localizedName}`}
+
+ );
+ })}
+
+ );
+ }
+}
+
+LocalesDropdown.propTypes = propTypes;
+LocalesDropdown.defaultProps = defaultProps;
+
+export default injectIntl(LocalesDropdown);
diff --git a/src/2.7.12/imports/ui/components/common/menu/component.jsx b/src/2.7.12/imports/ui/components/common/menu/component.jsx
new file mode 100644
index 00000000..e678b499
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/menu/component.jsx
@@ -0,0 +1,269 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { defineMessages, injectIntl } from "react-intl";
+import { Divider } from "@mui/material";
+import Icon from "/imports/ui/components/common/icon/component";
+import { SMALL_VIEWPORT_BREAKPOINT } from '/imports/ui/components/layout/enums';
+import KEY_CODES from '/imports/utils/keyCodes';
+
+import Styled from './styles';
+
+const intlMessages = defineMessages({
+ close: {
+ id: 'app.dropdown.close',
+ description: 'Close button label',
+ },
+ active: {
+ id: 'app.dropdown.list.item.activeLabel',
+ description: 'active item label',
+ },
+});
+
+class BBBMenu extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ anchorEl: null,
+ };
+
+ this.optsToMerge = {};
+ this.autoFocus = false;
+
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handleClick = this.handleClick.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ }
+
+ componentDidUpdate() {
+ const { anchorEl } = this.state;
+ const { open } = this.props;
+ if (open === false && anchorEl) {
+ this.setState({ anchorEl: null });
+ } else if (open === true && !anchorEl) {
+ this.setState({ anchorEl: this.anchorElRef });
+ }
+ }
+
+ handleKeyDown(event) {
+ const { anchorEl } = this.state;
+ const { isHorizontal } = this.props;
+ const isMenuOpen = Boolean(anchorEl);
+
+ const previousKey = isHorizontal ? KEY_CODES.ARROW_LEFT : KEY_CODES.ARROW_UP;
+ const nextKey = isHorizontal ? KEY_CODES.ARROW_RIGHT : KEY_CODES.ARROW_DOWN;
+
+ if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.which)) {
+ this.handleClose();
+ return;
+ }
+
+ if (isMenuOpen && [previousKey, nextKey].includes(event.which)) {
+ event.preventDefault();
+ event.stopPropagation();
+ const menuItems = Array.from(document.querySelectorAll('[data-key^="menuItem-"]'));
+ if (menuItems.length === 0) return;
+
+ const focusedIndex = menuItems.findIndex(item => item === document.activeElement);
+ const nextIndex = event.which === previousKey ? focusedIndex - 1 : focusedIndex + 1;
+ let indexToFocus = 0;
+ if (nextIndex < 0) {
+ indexToFocus = menuItems.length - 1;
+ } else if (nextIndex >= menuItems.length) {
+ indexToFocus = 0;
+ } else {
+ indexToFocus = nextIndex;
+ }
+
+ menuItems[indexToFocus].focus();
+ }
+ };
+
+ handleClick(event) {
+ this.setState({ anchorEl: event.currentTarget });
+ };
+
+ handleClose(event) {
+ const { onCloseCallback } = this.props;
+ this.setState({ anchorEl: null }, onCloseCallback());
+
+ if (event) {
+ event.persist();
+
+ if (event.type === 'click') {
+ setTimeout(() => {
+ document.activeElement.blur();
+ }, 0);
+ }
+ }
+ };
+
+ makeMenuItems() {
+ const { actions, selectedEmoji, intl, isHorizontal, isMobile, roundButtons, keepOpen } = this.props;
+
+ return actions?.map(a => {
+ const { dataTest, label, onClick, key, disabled, description, selected } = a;
+ const emojiSelected = key?.toLowerCase()?.includes(selectedEmoji?.toLowerCase());
+
+ let customStyles = {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ paddingTop: '12px',
+ paddingBottom: '12px',
+ marginLeft: '0px',
+ marginRight: '0px',
+ };
+
+ if (a.customStyles) {
+ customStyles = { ...customStyles, ...a.customStyles };
+ }
+
+ return [
+ a.dividerTop && ,
+ {
+ onClick();
+ const close = !keepOpen && !key?.includes('setstatus') && !key?.includes('back');
+ // prevent menu close for sub menu actions
+ if (close) this.handleClose(event);
+ event.stopPropagation();
+ }}>
+
+ {a.icon ? : null}
+ {label}
+ {description && {`${description}${selected ? ` - ${intl.formatMessage(intlMessages.active)}` : ''}`}
}
+ {a.iconRight ? : null}
+
+ ,
+ a.divider &&
+ ];
+ }) ?? [];
+ }
+
+ render() {
+ const { anchorEl } = this.state;
+ const {
+ trigger,
+ intl,
+ customStyles,
+ dataTest,
+ opts,
+ accessKey,
+ open,
+ renderOtherComponents,
+ customAnchorEl,
+ hasRoundedCorners,
+ overrideMobileStyles,
+ isHorizontal,
+ } = this.props;
+ const actionsItems = this.makeMenuItems();
+
+ const roundedCornersStyles = { borderRadius: '1.8rem' };
+ let menuStyles = { zIndex: 999 };
+
+ if (customStyles) {
+ menuStyles = { ...menuStyles, ...customStyles };
+ }
+
+ if (isHorizontal) {
+ const horizontalStyles = { display: 'flex' };
+ menuStyles = { ...menuStyles, ...horizontalStyles};
+ }
+
+ return (
+ <>
+ {
+ e.persist();
+ const firefoxInputSource = !([1, 5].includes(e.nativeEvent.mozInputSource)); // 1 = mouse, 5 = touch (firefox only)
+ const chromeInputSource = !(['mouse', 'touch'].includes(e.nativeEvent.pointerType));
+
+ this.optsToMerge.autoFocus = firefoxInputSource && chromeInputSource;
+ this.handleClick(e);
+ }}
+ onKeyPress={(e) => {
+ e.persist();
+ if (e.which !== KEY_CODES.ENTER) return null;
+ this.handleClick(e);
+ }}
+ accessKey={accessKey}
+ ref={(ref) => this.anchorElRef = ref}
+ role="button"
+ tabIndex={-1}
+ >
+ {trigger}
+
+
+
+ {actionsItems}
+ {renderOtherComponents}
+ {!overrideMobileStyles && anchorEl && window.innerWidth < SMALL_VIEWPORT_BREAKPOINT &&
+
+ }
+
+ >
+ );
+ }
+}
+
+BBBMenu.defaultProps = {
+ opts: {
+ id: "default-dropdown-menu",
+ autoFocus: false,
+ keepMounted: true,
+ transitionDuration: 0,
+ elevation: 3,
+ getcontentanchorel: null,
+ fullwidth: "true",
+ anchorOrigin: { vertical: 'top', horizontal: 'right' },
+ transformorigin: { vertical: 'top', horizontal: 'right' },
+ },
+ onCloseCallback: () => { },
+ dataTest: '',
+};
+
+BBBMenu.propTypes = {
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func.isRequired,
+ }).isRequired,
+
+ trigger: PropTypes.element.isRequired,
+
+ actions: PropTypes.array.isRequired,
+
+ onCloseCallback: PropTypes.func,
+ dataTest: PropTypes.string,
+ open: PropTypes.bool,
+ customStyles: PropTypes.object,
+ opts: PropTypes.object,
+ accessKey: PropTypes.string,
+};
+
+export default injectIntl(BBBMenu);
diff --git a/src/2.7.12/imports/ui/components/common/menu/styles.js b/src/2.7.12/imports/ui/components/common/menu/styles.js
new file mode 100644
index 00000000..6f586b33
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/menu/styles.js
@@ -0,0 +1,129 @@
+import styled from 'styled-components';
+import Button from "/imports/ui/components/common/button/component";
+import Icon from '/imports/ui/components/common/icon/component';
+import MenuItem from "@mui/material/MenuItem";
+import { colorWhite, colorPrimary } from '/imports/ui/stylesheets/styled-components/palette';
+import { fontSizeLarge } from '/imports/ui/stylesheets/styled-components/typography';
+import { mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
+import Menu from "@mui/material/Menu";
+
+const MenuWrapper = styled(Menu)`
+ ${({ isMobile }) => isMobile && `
+ flex-direction: column;
+ align-items: center;
+ padding: .5rem 0;
+ `}
+
+ ${({ $isHorizontal, isMobile }) => ($isHorizontal || isMobile) && `
+ ul {
+ display: flex;
+ }
+
+ li:hover {
+ background-color: unset !important;
+ }
+
+ `}
+`;
+
+const MenuItemWrapper = styled.div`
+ display: flex;
+ flex-flow: row;
+ width: 100%;
+ align-items: center;
+`;
+
+const Option = styled.div`
+ line-height: 1;
+ margin-right: 1.65rem;
+ margin-left: .5rem;
+ white-space: normal;
+ overflow-wrap: anywhere;
+ padding: .1rem 0;
+
+ [dir="rtl"] & {
+ margin-right: .5rem;
+ margin-left: 1.65rem;
+ }
+
+ ${({ isHorizontal, isMobile }) => (isHorizontal || isMobile) && `
+ margin-right: 0;
+ margin-left: 0;
+ `}
+`;
+
+const CloseButton = styled(Button)`
+ position: fixed;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ height: 5rem;
+ background-color: ${colorWhite};
+ padding: 1rem;
+
+ border-radius: 0;
+ z-index: 1011;
+ font-size: calc(${fontSizeLarge} * 1.1);
+ box-shadow: 0 0 0 2rem ${colorWhite} !important;
+ border: ${colorWhite} !important;
+ cursor: pointer !important;
+
+ @media ${mediumUp} {
+ display: none;
+ }
+`;
+
+const IconRight = styled(Icon)`
+ display: flex;
+ justify-content: flex-end;
+ flex: 1;
+`;
+
+const BBBMenuItem = styled(MenuItem)`
+ transition: none !important;
+ font-size: 90% !important;
+
+ &:focus,
+ &:hover {
+ i {
+ color: #FFF;
+ }
+ color: #FFF !important;
+ background-color: ${colorPrimary} !important;
+ }
+
+ ${({ emoji }) => emoji === 'yes' && `
+ div,
+ i {
+ color: ${colorPrimary};
+ }
+
+ &:focus,
+ &:hover {
+ div,
+ i {
+ color: #FFF ;
+ }
+ }
+ `}
+ ${({ $roundButtons }) => $roundButtons && `
+ &:focus,
+ &:hover {
+ background-color: ${colorWhite} !important;
+ div div div {
+ background-color: ${colorPrimary} !important;
+ border: 1px solid ${colorPrimary} !important;
+ }
+ }
+ `}
+`;
+
+export default {
+ MenuWrapper,
+ MenuItemWrapper,
+ Option,
+ CloseButton,
+ IconRight,
+ BBBMenuItem,
+};
diff --git a/src/2.7.12/imports/ui/components/common/modal/base/component.jsx b/src/2.7.12/imports/ui/components/common/modal/base/component.jsx
new file mode 100644
index 00000000..1ff67802
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/base/component.jsx
@@ -0,0 +1,55 @@
+import React, { useCallback, useEffect } from 'react';
+import Styled from './styles';
+
+const BaseModal = (props) => {
+ const {
+ setIsOpen,
+ modalName,
+ children,
+ isOpen,
+ onRequestClose,
+ className,
+ overlayClassName,
+ dataTest,
+ priority,
+ } = props;
+
+ const closeEventHandler = useCallback(() => {
+ setIsOpen(false);
+ }, []);
+ useEffect(() => {
+ // Only add event listener if name is specified
+ if (!modalName) return () => null;
+
+ const closeEventName = `CLOSE_MODAL_${modalName.toUpperCase()}`;
+
+ // Listen to close event on mount
+ document.addEventListener(closeEventName, closeEventHandler);
+
+ // Remove listener on unmount
+ return () => {
+ document.removeEventListener(closeEventName, closeEventHandler);
+ };
+ }, []);
+ const priorityValue = priority || 'low';
+
+ return (
+ document.querySelector('#modals-container')}
+ isOpen={isOpen}
+ onRequestClose={onRequestClose}
+ className={className}
+ overlayClassName={overlayClassName}
+ shouldReturnFocusAfterClose={false}
+ data={{
+ test: dataTest,
+ }}
+ {...props}
+ >
+ {children}
+
+ );
+};
+
+export default { BaseModal };
diff --git a/src/2.7.12/imports/ui/components/common/modal/base/styles.js b/src/2.7.12/imports/ui/components/common/modal/base/styles.js
new file mode 100644
index 00000000..9cd1c837
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/base/styles.js
@@ -0,0 +1,25 @@
+import styled from 'styled-components';
+import { ModalScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scrollable';
+import { borderRadius } from '/imports/ui/stylesheets/styled-components/general';
+import { smallOnly, mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
+import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
+
+const BaseModal = styled(ModalScrollboxVertical)`
+ max-width: 60vw;
+ max-height: 100%;
+ border-radius: ${borderRadius};
+ background: ${colorWhite};
+ overflow: auto;
+
+ @media ${smallOnly} {
+ max-width: 95vw;
+ }
+
+ @media ${mediumUp} {
+ max-width: 80vw;
+ }
+`;
+
+export default {
+ BaseModal,
+};
diff --git a/src/2.7.12/imports/ui/components/common/modal/confirmation/component.jsx b/src/2.7.12/imports/ui/components/common/modal/confirmation/component.jsx
new file mode 100644
index 00000000..ae6eb3ec
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/confirmation/component.jsx
@@ -0,0 +1,119 @@
+import React, { Component } from 'react';
+import { defineMessages } from 'react-intl';
+import PropTypes from 'prop-types';
+import Styled from './styles';
+
+const messages = defineMessages({
+ yesLabel: {
+ id: 'app.confirmationModal.yesLabel',
+ description: 'confirm button label',
+ },
+ noLabel: {
+ id: 'app.endMeeting.noLabel',
+ description: 'cancel confirm button label',
+ },
+});
+
+const propTypes = {
+ confirmButtonColor: PropTypes.string,
+ disableConfirmButton: PropTypes.bool,
+ description: PropTypes.string,
+};
+
+const defaultProps = {
+ confirmButtonColor: 'primary',
+ disableConfirmButton: false,
+ description: '',
+};
+
+class ConfirmationModal extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ checked: false,
+ };
+ }
+
+ render() {
+ const {
+ intl,
+ setIsOpen,
+ onConfirm,
+ title,
+ titleMessageId,
+ titleMessageExtra,
+ checkboxMessageId,
+ confirmButtonColor,
+ confirmButtonLabel,
+ confirmButtonDataTest,
+ confirmParam,
+ disableConfirmButton,
+ description,
+ isOpen,
+ onRequestClose,
+ priority,
+ } = this.props;
+
+ const {
+ checked,
+ } = this.state;
+
+ const hasCheckbox = !!checkboxMessageId;
+
+ return (
+ setIsOpen(false)}
+ contentLabel={title}
+ title={title || intl.formatMessage({ id: titleMessageId }, { 0: titleMessageExtra })}
+ {...{
+ isOpen,
+ onRequestClose,
+ priority,
+ }}
+ >
+
+
+
+ {description}
+
+ { hasCheckbox ? (
+
+ this.setState({ checked: !checked })}
+ checked={checked}
+ aria-label={intl.formatMessage({ id: checkboxMessageId })}
+ />
+ {intl.formatMessage({ id: checkboxMessageId })}
+
+ ) : null }
+
+
+
+ {
+ onConfirm(confirmParam, checked);
+ setIsOpen(false);
+ }}
+ />
+ setIsOpen(false)}
+ />
+
+
+
+ );
+ }
+}
+
+ConfirmationModal.propTypes = propTypes;
+ConfirmationModal.defaultProps = defaultProps;
+
+export default ConfirmationModal;
diff --git a/src/2.7.12/imports/ui/components/common/modal/confirmation/styles.js b/src/2.7.12/imports/ui/components/common/modal/confirmation/styles.js
new file mode 100644
index 00000000..58c8fdcd
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/confirmation/styles.js
@@ -0,0 +1,81 @@
+import styled from 'styled-components';
+import Button from '/imports/ui/components/common/button/component';
+import ModalSimple from '/imports/ui/components/common/modal/simple/component';
+import {
+ smPaddingX,
+ mdPaddingX,
+ lgPaddingY,
+ jumboPaddingY,
+} from '/imports/ui/stylesheets/styled-components/general';
+import { colorGray } from '/imports/ui/stylesheets/styled-components/palette';
+import { lineHeightBase } from '/imports/ui/stylesheets/styled-components/typography';
+
+const ConfirmationModal = styled(ModalSimple)`
+ padding: ${mdPaddingX};
+`;
+
+const Container = styled.div`
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ padding: 0;
+ margin-top: 0;
+ margin: auto;
+`;
+
+const Description = styled.div`
+ text-align: center;
+ line-height: ${lineHeightBase};
+ color: ${colorGray};
+ margin-bottom: ${jumboPaddingY};
+`;
+
+const DescriptionText = styled.span`
+ white-space: pre-line;
+`;
+
+const Checkbox = styled.input`
+ position: relative;
+ top: 0.134rem;
+ margin-right: 0.5rem;
+
+ [dir="rtl"] & {
+ margin-right: 0;
+ margin-left: 0.5rem;
+ }
+`;
+
+const Footer = styled.div`
+ display:flex;
+ margin-bottom: ${lgPaddingY};
+`;
+
+const ConfirmationButton = styled(Button)`
+ padding-right: ${jumboPaddingY};
+ padding-left: ${jumboPaddingY};
+ margin: 0 ${smPaddingX} 0 0;
+
+ [dir="rtl"] & {
+ margin: 0 0 0 ${smPaddingX};
+ }
+`;
+
+const CancelButton = styled(ConfirmationButton)`
+ margin: 0;
+`;
+
+const Label = styled.label`
+ display: block;
+`;
+
+export default {
+ ConfirmationModal,
+ Container,
+ Description,
+ DescriptionText,
+ Checkbox,
+ Footer,
+ ConfirmationButton,
+ CancelButton,
+ Label,
+};
diff --git a/src/2.7.12/imports/ui/components/common/modal/fullscreen/component.jsx b/src/2.7.12/imports/ui/components/common/modal/fullscreen/component.jsx
new file mode 100644
index 00000000..e0da8fdf
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/fullscreen/component.jsx
@@ -0,0 +1,149 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+import Styled from './styles';
+
+const intlMessages = defineMessages({
+ modalClose: {
+ id: 'app.modal.close',
+ description: 'Close',
+ },
+ modalCloseDescription: {
+ id: 'app.modal.close.description',
+ description: 'Disregards changes and closes the modal',
+ },
+ modalDone: {
+ id: 'app.modal.confirm',
+ description: 'Close',
+ },
+ modalDoneDescription: {
+ id: 'app.modal.confirm.description',
+ description: 'Disregards changes and closes the modal',
+ },
+ newTabLabel: {
+ id: 'app.modal.newTab',
+ description: 'aria label used to indicate opening a new window',
+ },
+});
+
+const propTypes = {
+ title: PropTypes.string.isRequired,
+ confirm: PropTypes.shape({
+ callback: PropTypes.func.isRequired,
+ disabled: PropTypes.bool,
+ }),
+ dismiss: PropTypes.shape({
+ callback: PropTypes.func,
+ disabled: PropTypes.bool,
+ }),
+ preventClosing: PropTypes.bool,
+ shouldCloseOnOverlayClick: PropTypes.bool,
+};
+
+const defaultProps = {
+ shouldCloseOnOverlayClick: false,
+ confirm: {
+ disabled: false,
+ },
+ dismiss: {
+ callback: () => {},
+ disabled: false,
+ },
+ preventClosing: false,
+};
+
+class ModalFullscreen extends PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.handleAction = this.handleAction.bind(this);
+ }
+
+ handleAction(name) {
+ const { confirm, dismiss } = this.props;
+ const { callback: callBackConfirm } = confirm;
+ const { callback: callBackDismiss } = dismiss;
+
+ let callback;
+
+ switch (name) {
+ case 'confirm':
+ callback = callBackConfirm;
+ break;
+ case 'dismiss':
+ callback = callBackDismiss;
+ break;
+ default:
+ break;
+ }
+
+ return callback();
+ }
+
+ render() {
+ const {
+ intl,
+ title,
+ confirm,
+ dismiss,
+ className,
+ children,
+ isOpen,
+ preventClosing,
+ ...otherProps
+ } = this.props;
+
+ const popoutIcon = confirm.icon === 'popout_window';
+ let confirmAriaLabel = `${confirm.label || intl.formatMessage(intlMessages.modalDone)} `;
+ if (popoutIcon) {
+ confirmAriaLabel = `${confirmAriaLabel} ${intl.formatMessage(intlMessages.newTabLabel)}`;
+ }
+
+ return (
+
+
+ {title}
+
+ this.handleAction('dismiss')}
+ aria-describedby="modalDismissDescription"
+ color="secondary"
+ />
+ this.handleAction('confirm')}
+ aria-describedby="modalConfirmDescription"
+ icon={confirm.icon || null}
+ iconRight={popoutIcon}
+ popout={popoutIcon ? 'popout' : 'simple'}
+ />
+
+
+
+ {children}
+
+ {intl.formatMessage(intlMessages.modalCloseDescription)}
+ {intl.formatMessage(intlMessages.modalDoneDescription)}
+
+ );
+ }
+}
+
+ModalFullscreen.propTypes = propTypes;
+ModalFullscreen.defaultProps = defaultProps;
+
+export default injectIntl(ModalFullscreen);
diff --git a/src/2.7.12/imports/ui/components/common/modal/fullscreen/styles.js b/src/2.7.12/imports/ui/components/common/modal/fullscreen/styles.js
new file mode 100644
index 00000000..66467ccc
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/fullscreen/styles.js
@@ -0,0 +1,93 @@
+import styled from 'styled-components';
+import Styled from '../base/component';
+import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
+import Button from '/imports/ui/components/common/button/component';
+import {
+ borderSize,
+ smPaddingX,
+} from '/imports/ui/stylesheets/styled-components/general';
+import {
+ lineHeightComputed,
+ headingsFontWeight,
+ fontSizeLarger,
+} from '/imports/ui/stylesheets/styled-components/typography';
+import {
+ colorGrayLightest,
+ colorText,
+} from '/imports/ui/stylesheets/styled-components/palette';
+
+const FullscreenModal = styled(Styled.BaseModal)`
+ outline: transparent;
+ outline-width: ${borderSize};
+ outline-style: solid;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ align-self: flex-start;
+ padding: calc(${lineHeightComputed} / 2) ${lineHeightComputed};
+
+ @media ${smallOnly} {
+ width: 100%;
+ }
+`;
+
+const Header = styled.header`
+ display: flex;
+ padding: ${lineHeightComputed} 0;
+ border-bottom: ${borderSize} solid ${colorGrayLightest};
+`;
+
+const Title = styled.h1`
+ min-width: 0;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 1;
+ margin: 0;
+ font-size: ${fontSizeLarger};
+ font-weight: ${headingsFontWeight};
+`;
+
+const Actions = styled.div`
+ flex: 0 1 35%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+`;
+
+const Content = styled.div`
+ color: ${colorText};
+ font-weight: normal;
+ padding: ${lineHeightComputed} 0;
+`;
+
+const DismissButton = styled(Button)`
+ flex: 0 1 48%;
+`;
+
+const ConfirmButton = styled(Button)`
+ flex: 0 1 48%;
+
+ ${({ popout }) => popout === 'popout' && `
+ & > i {
+ bottom: ${borderSize};
+ left: ${smPaddingX};
+
+ [dir="rtl"] & {
+ left: -0.5rem;
+ transform: rotateY(180deg);
+ }
+ }
+ `}
+`;
+
+export default {
+ FullscreenModal,
+ Header,
+ Title,
+ Actions,
+ Content,
+ DismissButton,
+ ConfirmButton,
+};
diff --git a/src/2.7.12/imports/ui/components/common/modal/header/component.jsx b/src/2.7.12/imports/ui/components/common/modal/header/component.jsx
new file mode 100644
index 00000000..111d6a35
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/header/component.jsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import Styled from './styles';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ hideBorder: PropTypes.bool,
+ headerPosition: PropTypes.string,
+ shouldShowCloseButton: PropTypes.bool,
+ modalDismissDescription: PropTypes.string,
+ closeButtonProps: PropTypes.shape({
+ label: PropTypes.string,
+ 'aria-label': PropTypes.string,
+ onClick: PropTypes.func,
+ }),
+};
+
+const defaultProps = {
+ hideBorder: true,
+ headerPosition: 'inner',
+ shouldShowCloseButton: true,
+ modalDismissDescription: '',
+ closeButtonProps: {},
+};
+
+class Header extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const {
+ children,
+ closeButtonProps,
+ headerPosition,
+ hideBorder,
+ modalDismissDescription,
+ shouldShowCloseButton,
+ ...other
+ } = this.props;
+
+ if (!shouldShowCloseButton && !children) return null;
+
+ const headerOnTop = headerPosition === 'top';
+ const innerHeader = headerPosition === 'inner';
+
+ return (
+
+
+ {children}
+
+ {shouldShowCloseButton ? (
+
+ ) : null}
+ {modalDismissDescription}
+
+ );
+ }
+};
+
+Header.propTypes = propTypes;
+Header.defaultProps = defaultProps;
+
+export default Header;
diff --git a/src/2.7.12/imports/ui/components/common/modal/header/styles.js b/src/2.7.12/imports/ui/components/common/modal/header/styles.js
new file mode 100644
index 00000000..927b667d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/header/styles.js
@@ -0,0 +1,94 @@
+import styled from 'styled-components';
+import Button from '/imports/ui/components/common/button/component';
+import { TitleElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
+import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
+import {
+ colorGrayDark,
+ colorGrayLighter,
+ colorText,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import {
+ mdPaddingX,
+ borderSize,
+} from '/imports/ui/stylesheets/styled-components/general';
+import {
+ fontSizeBase,
+ fontSizeLarge,
+ headingsFontWeight,
+ lineHeightComputed,
+} from '/imports/ui/stylesheets/styled-components/typography';
+
+const Header = styled.header`
+ margin: 0;
+ padding: 0;
+ border: none;
+ display: grid;
+
+ ${({ $headerOnTop }) => $headerOnTop && `
+ grid-template-columns: auto min-content;
+ grid-template-rows: min-content;
+ `}
+
+ ${({ $innerHeader }) => $innerHeader && `
+ grid-template-columns: auto;
+ grid-template-rows: min-content min-content;
+ `}
+
+ ${({ $hideBorder }) => !$hideBorder && `
+ padding: calc(${lineHeightComputed} / 2) 0;
+ border-bottom: ${borderSize} solid ${colorGrayLighter};
+ `}
+`;
+
+const Title = styled(TitleElipsis)`
+ display: block;
+ text-align: center;
+ font-weight: ${headingsFontWeight};
+ font-size: calc(${fontSizeLarge} + 0.05rem);
+ color: ${colorGrayDark};
+ white-space: normal;
+ margin: 0;
+ line-height: calc(${lineHeightComputed} * 2);
+
+ @media ${smallOnly} {
+ font-size: ${fontSizeBase};
+ padding: 0 ${mdPaddingX};
+ }
+
+ ${({ $headerOnTop }) => $headerOnTop && `
+ grid-area: 1 / 1 / 2 / 3;
+ `}
+
+ ${({ $innerHeader }) => $innerHeader && `
+ grid-area: 2 / 1 / 3 / 2;
+ `}
+
+ ${({ $hasMarginBottom }) => $hasMarginBottom && `
+ margin-bottom: ${mdPaddingX};
+ `}
+`;
+
+const DismissButton = styled(Button)`
+ & > span:first-child {
+ border-color: transparent;
+ background-color: transparent;
+
+ & > i { color: ${colorText}; }
+ }
+
+ ${({ $headerOnTop }) => $headerOnTop && `
+ grid-area: 1 / 2 / 2 / 3;
+ `}
+
+ ${({ $innerHeader }) => $innerHeader && `
+ grid-area: 1 / 1 / 2 / 2;
+ `}
+
+ justify-self: end;
+`;
+
+export default {
+ Header,
+ Title,
+ DismissButton,
+};
diff --git a/src/2.7.12/imports/ui/components/common/modal/random-user/component.jsx b/src/2.7.12/imports/ui/components/common/modal/random-user/component.jsx
new file mode 100644
index 00000000..11b31ec7
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/random-user/component.jsx
@@ -0,0 +1,216 @@
+import React, { Component } from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import ModalSimple from '/imports/ui/components/common/modal/simple/component';
+import AudioService from '/imports/ui/components/audio/service';
+import Styled from './styles';
+
+const SELECT_RANDOM_USER_COUNTDOWN = Meteor.settings.public.selectRandomUser.countdown;
+
+const messages = defineMessages({
+ noViewers: {
+ id: 'app.modal.randomUser.noViewers.description',
+ description: 'Label displayed when no viewers are avaiable',
+ },
+ selected: {
+ id: 'app.modal.randomUser.selected.description',
+ description: 'Label shown to the selected user',
+ },
+ randUserTitle: {
+ id: 'app.modal.randomUser.title',
+ description: 'Modal title label',
+ },
+ whollbeSelected: {
+ id: 'app.modal.randomUser.who',
+ description: 'Label shown during the selection',
+ },
+ onlyOneViewerTobeSelected: {
+ id: 'app.modal.randomUser.alone',
+ description: 'Label shown when only one viewer to be selected',
+ },
+ reselect: {
+ id: 'app.modal.randomUser.reselect.label',
+ description: 'select new random user button label',
+ },
+ ariaModalTitle: {
+ id: 'app.modal.randomUser.ariaLabel.title',
+ description: 'modal title displayed to screen reader',
+ },
+});
+
+const propTypes = {
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func.isRequired,
+ }).isRequired,
+ numAvailableViewers: PropTypes.number.isRequired,
+ randomUserReq: PropTypes.func.isRequired,
+};
+
+class RandomUserSelect extends Component {
+ constructor(props) {
+ super(props);
+
+ if (props.currentUser.presenter) {
+ props.randomUserReq();
+ }
+
+ if (SELECT_RANDOM_USER_COUNTDOWN) {
+ this.state = {
+ count: 0,
+ };
+ this.play = this.play.bind(this);
+ }
+ }
+
+ iterateSelection() {
+ if (this.props.mappedRandomlySelectedUsers.length > 1) {
+ const that = this;
+ setTimeout(delay(that.props.mappedRandomlySelectedUsers, 1), that.props.mappedRandomlySelectedUsers[1][1]);
+ function delay(arr, num) {
+ that.setState({
+ count: num,
+ });
+ if (num < that.props.mappedRandomlySelectedUsers.length - 1) {
+ setTimeout(() => { delay(arr, num + 1); }, arr[num + 1][1]);
+ }
+ }
+ }
+ }
+
+ componentDidMount() {
+ const { keepModalOpen, toggleKeepModalOpen, currentUser } = this.props;
+
+ if (currentUser.presenter && !keepModalOpen) {
+ toggleKeepModalOpen();
+ }
+
+ if (SELECT_RANDOM_USER_COUNTDOWN && !currentUser.presenter) {
+ this.iterateSelection();
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (SELECT_RANDOM_USER_COUNTDOWN) {
+ if (this.props.currentUser.presenter && this.state.count === 0) {
+ this.iterateSelection();
+ }
+
+ if ((prevState.count !== this.state.count) && this.props.keepModalOpen) {
+ this.play();
+ }
+ }
+ }
+
+ play() {
+ AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
+ + Meteor.settings.public.app.basename
+ + Meteor.settings.public.app.instanceId}`
+ + '/resources/sounds/Poll.mp3');
+ }
+
+ reselect() {
+ if (SELECT_RANDOM_USER_COUNTDOWN) {
+ this.setState({
+ count: 0,
+ });
+ }
+ this.props.randomUserReq();
+ }
+
+ render() {
+ const {
+ keepModalOpen,
+ toggleKeepModalOpen,
+ intl,
+ setIsOpen,
+ numAvailableViewers,
+ currentUser,
+ clearRandomlySelectedUser,
+ mappedRandomlySelectedUsers,
+ isOpen,
+ priority,
+ } = this.props;
+
+ const counter = SELECT_RANDOM_USER_COUNTDOWN ? this.state.count : 0;
+ if (mappedRandomlySelectedUsers.length < counter + 1) return null;
+
+ const selectedUser = SELECT_RANDOM_USER_COUNTDOWN ? mappedRandomlySelectedUsers[counter][0] :
+ mappedRandomlySelectedUsers[mappedRandomlySelectedUsers.length - 1][0];
+ const countDown = SELECT_RANDOM_USER_COUNTDOWN ?
+ mappedRandomlySelectedUsers.length - this.state.count - 1 : 0;
+
+ let viewElement;
+ let title;
+
+ const amISelectedUser = currentUser.userId === selectedUser.userId;
+ if (numAvailableViewers < 1 || (currentUser.presenter && amISelectedUser)) { // there's no viewers to select from,
+ // or when you are the presenter but selected, which happens when the presenter ability is passed to somebody
+ // and people are entering and leaving the meeting
+ // display modal informing presenter that there's no viewers to select from
+ viewElement = (
+
+
+ {intl.formatMessage(messages.noViewers)}
+
+
+ );
+ title = intl.formatMessage(messages.randUserTitle);
+ } else { // viewers are available
+ if (!selectedUser) return null; // rendering triggered before selectedUser is available
+
+ // display modal with random user selection
+ viewElement = (
+
+
+ {selectedUser.name.slice(0, 2)}
+
+
+ {selectedUser.name}
+
+ {currentUser.presenter
+ && countDown === 0
+ && (
+ this.reselect()}
+ data-test="selectAgainRadomUser"
+ />
+ )}
+
+ );
+ title = countDown == 0
+ ? amISelectedUser
+ ? `${intl.formatMessage(messages.selected)}`
+ : numAvailableViewers == 1 && currentUser.presenter
+ ? `${intl.formatMessage(messages.onlyOneViewerTobeSelected)}`
+ : `${intl.formatMessage(messages.randUserTitle)}`
+ : `${intl.formatMessage(messages.whollbeSelected)} ${countDown}`;
+ }
+ if (keepModalOpen) {
+ return (
+ {
+ if (currentUser.presenter) clearRandomlySelectedUser();
+ toggleKeepModalOpen();
+ setIsOpen(false);
+ }}
+ contentLabel={intl.formatMessage(messages.ariaModalTitle)}
+ title={title}
+ {...{
+ isOpen,
+ priority,
+ }}
+ >
+ {viewElement}
+
+ );
+ } else {
+ return null;
+ }
+ }
+}
+
+RandomUserSelect.propTypes = propTypes;
+export default injectIntl(RandomUserSelect);
diff --git a/src/2.7.12/imports/ui/components/common/modal/random-user/container.jsx b/src/2.7.12/imports/ui/components/common/modal/random-user/container.jsx
new file mode 100644
index 00000000..5c92e9e2
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/random-user/container.jsx
@@ -0,0 +1,106 @@
+import React, { useContext } from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import Meetings from '/imports/api/meetings';
+import Users from '/imports/api/users';
+import Auth from '/imports/ui/services/auth';
+import { makeCall } from '/imports/ui/services/api';
+import RandomUserSelect from './component';
+import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
+import logger from '/imports/startup/client/logger';
+
+const SELECT_RANDOM_USER_ENABLED = Meteor.settings.public.selectRandomUser.enabled;
+
+// A value that is used by component to remember
+// whether it should be open or closed after a render
+let keepModalOpen = true;
+
+// A value that stores the previous indicator
+let updateIndicator = 1;
+
+const toggleKeepModalOpen = () => { keepModalOpen = ! keepModalOpen; };
+
+const RandomUserSelectContainer = (props) => {
+ const usingUsersContext = useContext(UsersContext);
+ const { users } = usingUsersContext;
+ const { randomlySelectedUser } = props;
+
+ let mappedRandomlySelectedUsers = [];
+
+ const currentUser = {
+ userId: Auth.userID,
+ presenter: users[Auth.meetingID][Auth.userID].presenter
+ };
+
+ try {
+ if (!currentUser.presenter // this functionality does not bother presenter
+ && (!keepModalOpen) // we only ween a change if modal has been closed before
+ && (randomlySelectedUser[0][1] !== updateIndicator)// if tey are different, a user was generated
+ ) { keepModalOpen = true; } // reopen modal
+ if (!currentUser.presenter) { updateIndicator = randomlySelectedUser[0][1]; } // keep indicator up to date
+ } catch (err) {
+ logger.error({
+ logCode: 'Random_USer_Error',
+ extraInfo: {
+ stackTrace: err,
+ },
+ },
+ '\nIssue in Random User Select container caused by back-end crash'
+ + '\nValue of 6 randomly selected users was passed as '
+ + `{${randomlySelectedUser}}`
+ + '\nHowever, it is handled.'
+ + '\nError message:'
+ + `\n${err}`);
+ }
+
+ if (randomlySelectedUser) {
+ mappedRandomlySelectedUsers = randomlySelectedUser.map((ui) => {
+ const selectedUser = users[Auth.meetingID][ui[0]];
+ if (selectedUser){
+ return [{
+ userId: selectedUser.userId,
+ avatar: selectedUser.avatar,
+ color: selectedUser.color,
+ name: selectedUser.name,
+ }, ui[1]];
+ }
+ });
+ }
+
+ return (
+
+ );
+};
+export default withTracker(() => {
+ const viewerPool = Users.find({
+ meetingId: Auth.meetingID,
+ presenter: { $ne: true },
+ role: { $eq: 'VIEWER' },
+ }, {
+ fields: {
+ userId: 1,
+ },
+ }).fetch();
+
+ const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, {
+ fields: {
+ randomlySelectedUser: 1,
+ },
+ });
+
+ const randomUserReq = () => (SELECT_RANDOM_USER_ENABLED ? makeCall('setRandomUser') : null);
+
+ const clearRandomlySelectedUser = () => (SELECT_RANDOM_USER_ENABLED ? makeCall('clearRandomlySelectedUser') : null);
+
+ return ({
+ toggleKeepModalOpen,
+ numAvailableViewers: viewerPool.length,
+ randomUserReq,
+ clearRandomlySelectedUser,
+ randomlySelectedUser: meeting.randomlySelectedUser,
+ });
+})(RandomUserSelectContainer);
diff --git a/src/2.7.12/imports/ui/components/common/modal/random-user/styles.js b/src/2.7.12/imports/ui/components/common/modal/random-user/styles.js
new file mode 100644
index 00000000..c93c238e
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/random-user/styles.js
@@ -0,0 +1,57 @@
+import styled from 'styled-components';
+import Button from '/imports/ui/components/common/button/component';
+import {
+ fontSizeXXL,
+ headingsFontWeight,
+} from '/imports/ui/stylesheets/styled-components/typography';
+import {
+ mdPaddingX,
+ smPaddingX,
+} from '/imports/ui/stylesheets/styled-components/general';
+
+const ModalViewContainer = styled.div`
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+
+ & > div {
+ margin-bottom: 1rem;
+ }
+`;
+
+const ModalAvatar = styled.div`
+ height: 6rem;
+ width: 6rem;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: white;
+ font-size: ${fontSizeXXL};
+ font-weight: 400;
+ margin-bottom: ${smPaddingX};
+ text-transform: capitalize;
+`;
+
+const SelectedUserName = styled.div`
+ margin-bottom: ${mdPaddingX};
+ font-weight: ${headingsFontWeight};
+ font-size: 2rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ position: relative;
+ width: 100%;
+ text-align: center;
+`;
+
+const SelectButton = styled(Button)`
+ margin-bottom: ${mdPaddingX};
+`;
+
+export default {
+ ModalViewContainer,
+ ModalAvatar,
+ SelectedUserName,
+ SelectButton,
+};
diff --git a/src/2.7.12/imports/ui/components/common/modal/simple/component.jsx b/src/2.7.12/imports/ui/components/common/modal/simple/component.jsx
new file mode 100644
index 00000000..67356128
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/simple/component.jsx
@@ -0,0 +1,143 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+import FocusTrap from 'focus-trap-react';
+import Styled from './styles';
+
+const intlMessages = defineMessages({
+ modalClose: {
+ id: 'app.modal.close',
+ description: 'Close',
+ },
+ modalCloseDescription: {
+ id: 'app.modal.close.description',
+ description: 'Disregards changes and closes the modal',
+ },
+});
+
+const propTypes = {
+ title: PropTypes.string,
+ dismiss: PropTypes.shape({
+ callback: PropTypes.func,
+ }),
+ headerPosition: PropTypes.string,
+ shouldCloseOnOverlayClick: PropTypes.bool,
+ shouldShowCloseButton: PropTypes.bool,
+ overlayClassName: PropTypes.string,
+ modalisOpen: PropTypes.bool,
+};
+
+const defaultProps = {
+ title: '',
+ dismiss: {
+ callback: null,
+ },
+ shouldCloseOnOverlayClick: true,
+ shouldShowCloseButton: true,
+ overlayClassName: 'modalOverlay',
+ headerPosition: 'inner',
+ modalisOpen: false,
+};
+
+class ModalSimple extends Component {
+ constructor(props) {
+ super(props);
+ this.modalRef = React.createRef();
+ this.handleDismiss = this.handleDismiss.bind(this);
+ this.handleRequestClose = this.handleRequestClose.bind(this);
+ this.handleOutsideClick = this.handleOutsideClick.bind(this);
+ }
+
+ componentDidMount() {
+ document.addEventListener('mousedown', this.handleOutsideClick, false);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('mousedown', this.handleOutsideClick, false);
+ }
+
+ handleDismiss() {
+ const { modalHide, dismiss } = this.props;
+ if (!dismiss || !modalHide) return;
+ modalHide(dismiss.callback);
+ }
+
+ handleRequestClose(event) {
+ const { onRequestClose } = this.props;
+ const closeModal = onRequestClose || this.handleDismiss;
+
+ closeModal();
+
+ if (event && event.type === 'click') {
+ setTimeout(() => {
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+ }, 0);
+ }
+ }
+
+ handleOutsideClick(e) {
+ const { modalisOpen } = this.props;
+ if (this.modalRef.current && !this.modalRef.current.contains(e.target) && modalisOpen) {
+ this.handleRequestClose(e);
+ }
+ }
+
+ render() {
+ const {
+ id,
+ intl,
+ title,
+ hideBorder,
+ dismiss,
+ className,
+ modalisOpen,
+ onRequestClose,
+ shouldShowCloseButton,
+ contentLabel,
+ headerPosition,
+ 'data-test': dataTest,
+ children,
+ ...otherProps
+ } = this.props;
+
+ return (
+
+
+
+
+ {title || ''}
+
+
+ {children}
+
+
+
+
+ );
+ }
+}
+
+ModalSimple.propTypes = propTypes;
+ModalSimple.defaultProps = defaultProps;
+
+export default injectIntl(ModalSimple);
diff --git a/src/2.7.12/imports/ui/components/common/modal/simple/styles.js b/src/2.7.12/imports/ui/components/common/modal/simple/styles.js
new file mode 100644
index 00000000..c7319c51
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/modal/simple/styles.js
@@ -0,0 +1,37 @@
+import styled from 'styled-components';
+import Styled from '../base/component';
+import {
+ borderSize,
+ mdPaddingX,
+} from '/imports/ui/stylesheets/styled-components/general';
+import {
+ colorWhite,
+ colorText,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import ModalHeader from '/imports/ui/components/common/modal/header/component';
+
+const SimpleModal = styled(Styled.BaseModal)`
+ outline: transparent;
+ outline-width: ${borderSize};
+ outline-style: solid;
+ display: flex;
+ flex-direction: column;
+ padding: ${mdPaddingX};
+ box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.5);
+ background-color: ${colorWhite} !important;
+`;
+
+const Header = styled(ModalHeader)``;
+
+const Content = styled.div`
+ overflow: visible;
+ color: ${colorText};
+ font-weight: normal;
+ padding: 0;
+`;
+
+export default {
+ SimpleModal,
+ Header,
+ Content,
+};
diff --git a/src/2.7.12/imports/ui/components/common/radio/component.jsx b/src/2.7.12/imports/ui/components/common/radio/component.jsx
new file mode 100644
index 00000000..6aad46a1
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/radio/component.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import Base from '../checkbox/base';
+import Styled from './styles';
+
+export default class Radio extends Base {
+ render() {
+ const {
+ ariaLabel, ariaDesc, ariaDescribedBy, ariaLabelledBy, checked, disabled, label,
+ } = this.props;
+
+ const radio = (
+ }
+ icon={ }
+ disabled={disabled}
+ inputProps={{
+ 'aria-label': ariaLabel,
+ 'aria-describedby': ariaDescribedBy,
+ 'aria-labelledby': ariaLabelledBy,
+ }}
+ onChange={this.handleChange}
+ ref={this.element}
+ />
+ );
+
+ return (
+ <>
+ {label ? (
+
+ ) : radio}
+ {ariaDesc}
+ >
+ );
+ }
+}
diff --git a/src/2.7.12/imports/ui/components/common/radio/styles.js b/src/2.7.12/imports/ui/components/common/radio/styles.js
new file mode 100644
index 00000000..df52803b
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/radio/styles.js
@@ -0,0 +1,33 @@
+import styled from 'styled-components';
+import { colorText, colorSuccess } from '/imports/ui/stylesheets/styled-components/palette';
+import Icon from '/imports/ui/components/common/icon/component';
+import BaseRadio from '@mui/material/Radio';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import { styled as muiStyled } from '@mui/system';
+
+const Radio = muiStyled(BaseRadio)(() => ({
+ '&.Mui-checked': {
+ color: `${colorSuccess} !important`,
+ },
+}));
+
+const Label = muiStyled(FormControlLabel)(() => ({
+ '& .MuiFormControlLabel-label': {
+ fontFamily: 'inherit !important',
+ color: `${colorText} !important`,
+ },
+ '&.Mui-disabled': {
+ cursor: 'not-allowed !important',
+ },
+}));
+
+const RadioIcon = styled(Icon)``;
+
+const RadioIconChecked = styled(RadioIcon)``;
+
+export default {
+ RadioIcon,
+ RadioIconChecked,
+ Radio,
+ Label,
+};
diff --git a/src/2.7.12/imports/ui/components/common/switch/component.jsx b/src/2.7.12/imports/ui/components/common/switch/component.jsx
new file mode 100644
index 00000000..2d05e4ac
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/switch/component.jsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import Toggle from 'react-toggle';
+import { defineMessages, injectIntl } from 'react-intl';
+import Settings from '/imports/ui/services/settings';
+import Styled from './styles';
+
+const intlMessages = defineMessages({
+ on: {
+ id: 'app.switch.onLabel',
+ description: 'label for toggle switch on state',
+ },
+ off: {
+ id: 'app.switch.offLabel',
+ description: 'label for toggle switch off state',
+ },
+});
+
+const defaultProps = {
+ showToggleLabel: true,
+ invertColors: false,
+};
+
+class Switch extends Toggle {
+ render() {
+ const {
+ intl,
+ icons: _icons,
+ ariaLabelledBy,
+ ariaDescribedBy,
+ ariaLabel,
+ ariaDesc,
+ showToggleLabel,
+ invertColors,
+ disabled,
+ ...inputProps
+ } = this.props;
+
+ const { animations } = Settings.application;
+
+ const {
+ checked,
+ hasFocus,
+ } = this.state;
+
+ return (
+
+
+
+ {showToggleLabel ? intl.formatMessage(intlMessages.on) : null}
+
+
+ {showToggleLabel ? intl.formatMessage(intlMessages.off) : null}
+
+
+
+
+ { this.input = ref; }}
+ onFocus={this.handleFocus}
+ onBlur={this.handleBlur}
+ type="checkbox"
+ tabIndex="0"
+ disabled={disabled}
+ aria-label={ariaLabel}
+ aria-describedby={ariaDescribedBy}
+ />
+ {ariaDesc}
+
+ );
+ }
+}
+
+Switch.defaultProps = defaultProps;
+
+export default injectIntl(Switch);
diff --git a/src/2.7.12/imports/ui/components/common/switch/styles.js b/src/2.7.12/imports/ui/components/common/switch/styles.js
new file mode 100644
index 00000000..8750c2cc
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/switch/styles.js
@@ -0,0 +1,170 @@
+import styled, { css } from 'styled-components';
+import { borderSize } from '/imports/ui/stylesheets/styled-components/general';
+import { colorDanger, colorSuccess } from '/imports/ui/stylesheets/styled-components/palette';
+
+const Switch = styled.div`
+ &:hover,
+ &:focus,
+ &:focus-within {
+ outline: transparent;
+ outline-style: dotted;
+ outline-width: ${borderSize};
+ }
+
+ &:focus,
+ &:focus-within {
+ outline-style: solid;
+ }
+
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ background-color: transparent;
+ border: 0;
+ padding: 0;
+
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ -webkit-tap-highlight-color: rgba(0,0,0,0);
+ -webkit-tap-highlight-color: transparent;
+
+ ${({ disabled }) => disabled && `
+ cursor: not-allowed;
+ opacity: 0.5;
+ `}
+
+ ${({ disabled, animations }) => disabled && animations && `
+ transition: opacity 0.25s;
+ `}
+`;
+
+const ToggleTrack = styled.div`
+ overflow: hidden;
+ width: 3.5rem;
+ height: 1.5rem;
+ padding: 0;
+ border-radius: 2rem;
+ background-color: ${colorDanger};
+
+ [dir="rtl"] & {
+ width: 4rem;
+ }
+
+ ${({ animations }) => animations && `
+ transition: all 0.2s ease;
+ `}
+
+ ${({ checked }) => checked && `
+ background-color: ${colorSuccess};
+ `}
+
+ ${({ invertColors, checked }) => invertColors && !checked && `
+ background-color: ${colorSuccess} !important;
+ `}
+
+ ${({ invertColors, checked }) => invertColors && checked && `
+ background-color: ${colorDanger} !important;
+ `}
+
+`;
+
+const ToggleTrackCheck = styled.div`
+ position: absolute;
+ color: white;
+ width: 1rem;
+ line-height: 1.5rem;
+ font-size: 0.8rem;
+ left: 0.5rem;
+ opacity: 0;
+
+ [dir="rtl"] & {
+ left: 0.8rem;
+ }
+
+ ${({ animations }) => animations && `
+ transition: opacity 0.25s ease;
+ `}
+
+ ${({ checked }) => checked && `
+ opacity: 1;
+ transition: opacity calc(var(--enableAnimation) * 0.25s) ease;
+ `}
+`;
+
+const ToggleTrackX = styled.div`
+ position: absolute;
+ color: white;
+ width: 1rem;
+ line-height: 1.5rem;
+ font-size: 0.8rem;
+ left: 1.7rem;
+ opacity: 1;
+
+ [dir="rtl"] & {
+ left: 2.2rem;
+ }
+
+ ${({ animations }) => animations && `
+ transition: opacity 0.25s ease;
+ `}
+
+ ${({ checked }) => checked && `
+ opacity: 0;
+ `}
+`;
+
+const ToggleThumb = styled.div`
+ position: absolute;
+ top: 1px;
+ left: ${({ isRTL }) => isRTL ? '2.6rem' : '1px'};
+ width: 1.35rem;
+ height: 1.35rem;
+ border-radius: 50%;
+ background-color: #FAFAFA;
+ box-sizing: border-box;
+ box-shadow: 2px 0px 10px -1px rgba(0,0,0,0.4);
+
+ ${({ animations }) => animations && `
+ transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
+ `}
+
+ ${({ checked }) => checked && css`
+ left: ${({ isRTL }) => isRTL ? '1px' : '2.1rem' };
+ box-shadow: -2px 0px 10px -1px rgba(0,0,0,0.4);
+ `}
+
+ ${({ hasFocus }) => hasFocus && `
+ box-shadow: 0px 0px 2px 3px #0F70D7;
+ `}
+
+ ${({ disabled }) => !disabled && `
+ &:active{
+ box-shadow: 0px 0px 5px 5px #0F70D7;
+ }
+ `}
+`;
+
+const ScreenreaderInput = styled.input`
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+`;
+
+export default {
+ Switch,
+ ToggleTrack,
+ ToggleTrackCheck,
+ ToggleTrackX,
+ ToggleThumb,
+ ScreenreaderInput,
+};
diff --git a/src/2.7.12/imports/ui/components/common/toast/component.jsx b/src/2.7.12/imports/ui/components/common/toast/component.jsx
new file mode 100644
index 00000000..e5827977
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/toast/component.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { toast } from 'react-toastify';
+import Icon from '/imports/ui/components/common/icon/component';
+import Styled from './styles';
+
+const propTypes = {
+ icon: PropTypes.string,
+ message: PropTypes.node.isRequired,
+ type: PropTypes.oneOf(Object.values(toast.TYPE)).isRequired,
+};
+
+const defaultProps = {
+ icon: null,
+};
+
+const defaultIcons = {
+ [toast.TYPE.INFO]: 'help',
+ [toast.TYPE.SUCCESS]: 'checkmark',
+ [toast.TYPE.WARNING]: 'warning',
+ [toast.TYPE.ERROR]: 'close',
+ [toast.TYPE.DEFAULT]: 'about',
+};
+
+const Toast = ({
+ icon,
+ type,
+ message,
+ content,
+ small,
+}) => (
+
+
+
+
+
+
+ {message}
+
+
+ {content
+ ? (
+
+
+
+ {content}
+
+
+ ) : null}
+
+);
+
+export default Toast;
+
+Toast.propTypes = propTypes;
+Toast.defaultProps = defaultProps;
diff --git a/src/2.7.12/imports/ui/components/common/toast/container.jsx b/src/2.7.12/imports/ui/components/common/toast/container.jsx
new file mode 100644
index 00000000..ba94c8ff
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/toast/container.jsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import Styled from './styles';
+import Settings from '/imports/ui/services/settings';
+
+class ToastContainer extends React.Component {
+ // we never want this component to update since will break Toastify
+ shouldComponentUpdate() {
+ return false;
+ }
+
+ render() {
+ const { animations } = Settings.application;
+
+ return (
+ )}
+ autoClose={5000}
+ toastClassName="toastClass"
+ bodyClassName="toastBodyClass"
+ progressClassName="toastProgressClass"
+ newestOnTop={false}
+ hideProgressBar={false}
+ closeOnClick
+ pauseOnHover
+ />
+ );
+ }
+}
+
+export default ToastContainer;
diff --git a/src/2.7.12/imports/ui/components/common/toast/inject-notify/component.jsx b/src/2.7.12/imports/ui/components/common/toast/inject-notify/component.jsx
new file mode 100644
index 00000000..acbf1daa
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/toast/inject-notify/component.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { notify } from '/imports/ui/services/notification';
+
+const injectNotify = ComponentToWrap =>
+ props => ( );
+
+export default injectNotify;
diff --git a/src/2.7.12/imports/ui/components/common/toast/styles.js b/src/2.7.12/imports/ui/components/common/toast/styles.js
new file mode 100644
index 00000000..f054bff9
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/toast/styles.js
@@ -0,0 +1,210 @@
+import styled from 'styled-components';
+import { ToastContainer as Toastify } from 'react-toastify';
+import Icon from '/imports/ui/components/common/icon/component';
+import {
+ fontSizeSmallest,
+ fontSizeSmaller,
+ fontSizeSmall,
+ lineHeightComputed,
+} from '/imports/ui/stylesheets/styled-components/typography';
+import {
+ colorGrayDark,
+ toastDefaultColor,
+ toastDefaultBg,
+ toastInfoColor,
+ toastInfoBg,
+ toastSuccessColor,
+ toastSuccessBg,
+ toastErrorColor,
+ toastErrorBg,
+ toastWarningColor,
+ toastWarningBg,
+ colorGrayLighter,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import {
+ toastOffsetSm,
+ smPaddingX,
+ borderSizeSmall,
+ toastIconMd,
+ toastIconSm,
+ jumboPaddingY,
+} from '/imports/ui/stylesheets/styled-components/general';
+import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
+
+const CloseIcon = styled(Icon)`
+ background: transparent;
+ outline: none;
+ border: none;
+ cursor: pointer;
+ opacity: .5;
+ font-size: ${fontSizeSmallest};
+ color: ${colorGrayDark};
+ line-height: 0;
+ position: relative;
+ font-size: 70%;
+ left: ${toastOffsetSm};
+
+ [dir="rtl"] & {
+ left: auto;
+ right: ${toastOffsetSm};
+ }
+
+ ${({ animations }) => animations && `
+ transition: .3s ease;
+ `}
+
+ &:before {
+ margin: inherit inherit inherit -.4rem;
+
+ [dir="rtl"] & {
+ margin: inherit -.4rem inherit inherit;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ opacity: 1;
+ }
+
+ @media ${smallOnly} {
+ position: relative;
+ font-size: ${fontSizeSmaller};
+ left: auto;
+ }
+`;
+
+const ToastContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+
+ ${({ small }) => small && `
+ background-color: inherit;
+ `}
+`;
+
+const ToastIcon = styled.div`
+ align-self: flex-start;
+ margin: 0 ${smPaddingX} auto 0;
+ width: ${toastIconMd};
+ height: ${toastIconMd};
+ border-radius: 50%;
+ position: relative;
+ flex-shrink: 0;
+
+ [dir="rtl"] & {
+ margin: 0 0 auto ${smPaddingX};
+ }
+
+ & > i {
+ line-height: 0;
+ color: inherit;
+ position: absolute;
+ top: 50%;
+ width: 100%;
+ }
+
+ ${({ small }) => small && `
+ width: ${toastIconSm};
+ height: ${toastIconSm};
+ & > i {
+ font-size: 70%;
+ }
+ `}
+`;
+
+const ToastMessage = styled.div`
+ margin-top: auto;
+ margin-bottom: auto;
+ font-size: ${fontSizeSmall};
+ max-height: 15vh;
+ overflow: auto;
+
+ ${({ small }) => small && `
+ font-size: 80%;
+ `}
+`;
+
+const BackgroundColorInherit = styled.div`
+ position: relative;
+`;
+
+const Separator = styled.div`
+ position: relative;
+ width: 100%;
+ height: ${borderSizeSmall};
+ background-color: ${colorGrayLighter};
+ margin-top: calc(${lineHeightComputed} * .5);
+ margin-bottom: calc(${lineHeightComputed} * .5);
+`;
+
+const Toast = styled.div`
+ display: flex;
+
+ ${({ type }) => type === 'default' && `
+ & .toastIcon {
+ color: ${toastDefaultColor};
+ background-color: ${toastDefaultBg};
+ }
+ `}
+
+ ${({ type }) => type === 'error' && `
+ & .toastIcon {
+ color: ${toastErrorColor};
+ background-color: ${toastErrorBg};
+ }
+ `}
+
+ ${({ type }) => type === 'info' && `
+ & .toastIcon {
+ color: ${toastInfoColor};
+ background-color: ${toastInfoBg};
+ }
+ `}
+
+ ${({ type }) => type === 'success' && `
+ & .toastIcon {
+ color: ${toastSuccessColor};
+ background-color: ${toastSuccessBg};
+ }
+ `}
+
+ ${({ type }) => type === 'warning' && `
+ & .toastIcon {
+ color: ${toastWarningColor};
+ background-color: ${toastWarningBg};
+ }
+ `}
+`;
+
+const ToastifyContainer = styled(Toastify)`
+ z-index: 998;
+ position: fixed;
+ min-width: 20rem !important;
+ max-width: 23rem !important;
+ box-sizing: border-box;
+ right: ${jumboPaddingY};
+ left: auto;
+ top: 4.5rem;
+ max-height: 75vh;
+ overflow: hidden;
+
+ [dir="rtl"] & {
+ right: auto;
+ left: ${jumboPaddingY};
+ }
+
+ @media ${smallOnly} {
+ width: 75%;
+ }
+`;
+
+export default {
+ CloseIcon,
+ ToastContainer,
+ ToastIcon,
+ ToastMessage,
+ BackgroundColorInherit,
+ Separator,
+ Toast,
+ ToastifyContainer,
+};
diff --git a/src/2.7.12/imports/ui/components/common/tooltip/bbbtip.css b/src/2.7.12/imports/ui/components/common/tooltip/bbbtip.css
new file mode 100644
index 00000000..2a179d9b
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/tooltip/bbbtip.css
@@ -0,0 +1,17 @@
+.tippy-box[data-theme~='bbbtip']{
+ color:#fff;
+ background-color:#333333;
+ padding: .25rem .5rem;
+ border-radius: 4px;
+}
+
+.tippy-box[data-theme~='bbbtip']>.tippy-svg-arrow{
+ fill: #333333;
+ background-color: transparent;
+}
+
+.tippy-box[data-theme~='bbbtip']>.tippy-content{
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/src/2.7.12/imports/ui/components/common/tooltip/component.jsx b/src/2.7.12/imports/ui/components/common/tooltip/component.jsx
new file mode 100644
index 00000000..5aa3b5e8
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/tooltip/component.jsx
@@ -0,0 +1,200 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import { ESCAPE } from '/imports/utils/keyCodes';
+import Settings from '/imports/ui/services/settings';
+import Tippy, { roundArrow } from 'tippy.js';
+import 'tippy.js/dist/svg-arrow.css';
+import 'tippy.js/animations/shift-away.css';
+import './bbbtip.css';
+import BaseButton from '/imports/ui/components/common/button/base/component';
+import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji';
+import { uniqueId } from '/imports/utils/string-utils';
+
+const ANIMATION_DURATION = 350;
+const ANIMATION_DELAY = [150, 50];
+const DEFAULT_ANIMATION = 'shift-away';
+const ANIMATION_NONE = 'none';
+const TIP_OFFSET = [0, 10];
+
+const propTypes = {
+ title: PropTypes.string,
+ position: PropTypes.oneOf(['bottom','top']),
+ children: PropTypes.element.isRequired,
+ className: PropTypes.string,
+};
+
+const defaultProps = {
+ position: 'bottom',
+ className: null,
+ title: '',
+};
+
+class Tooltip extends Component {
+ static buttonComponentHasButtonEmoji(_component) {
+ return (
+ _component
+ && (_component.type === BaseButton)
+ && (_component.props)
+ && (_component.props.children)
+ && (typeof _component.props.children.find === 'function')
+ && (!!_component.props.children.find((_child) => (
+ _child && _child.type === ButtonEmoji
+ )))
+ );
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.tippySelectorId = uniqueId('tippy-');
+ this.onShow = this.onShow.bind(this);
+ this.onHide = this.onHide.bind(this);
+ this.handleEscapeHide = this.handleEscapeHide.bind(this);
+ }
+
+ componentDidMount() {
+ const {
+ position,
+ title,
+ delay,
+ placement,
+ } = this.props;
+
+ const { animations } = Settings.application;
+
+ const overridePlacement = placement ? placement : position;
+ let overrideDelay;
+ if (animations) {
+ overrideDelay = delay ? [delay, ANIMATION_DELAY[1]] : ANIMATION_DELAY;
+ } else {
+ overrideDelay = delay ? [delay, 0] : [ANIMATION_DELAY[0], 0];
+ }
+
+ const options = {
+ aria: null,
+ allowHTML: false,
+ animation: animations ? DEFAULT_ANIMATION : ANIMATION_NONE,
+ appendTo: document.body,
+ arrow: roundArrow,
+ boundary: 'window',
+ content: title,
+ delay: overrideDelay,
+ duration: animations ? ANIMATION_DURATION : 0,
+ interactive: true,
+ interactiveBorder: 10,
+ onShow: this.onShow,
+ onHide: this.onHide,
+ offset: TIP_OFFSET,
+ placement: overridePlacement,
+ touch: ['hold', 1000],
+ theme: 'bbbtip',
+ multiple: false,
+ };
+ this.tooltip = Tippy(`#${this.tippySelectorId}`, options);
+ }
+
+ componentDidUpdate() {
+ const { animations } = Settings.application;
+ const { title } = this.props;
+ const elements = document.querySelectorAll('[id^="tippy-"]');
+
+ Array.from(elements).filter((e) => {
+ const instance = e._tippy;
+
+ if (!instance) return false;
+
+ const animation = animations ? DEFAULT_ANIMATION : ANIMATION_NONE;
+
+ if (animation === instance.props.animation) return false;
+
+ return true;
+ }).forEach((e) => {
+ const instance = e._tippy;
+ const newProps = {
+ animation: animations
+ ? DEFAULT_ANIMATION : ANIMATION_NONE,
+ duration: animations ? ANIMATION_DURATION : 0,
+ };
+ if (!e.getAttribute("delay")) {
+ newProps["delay"] = animations ? ANIMATION_DELAY : [ANIMATION_DELAY[0], 0];
+ }
+ instance.setProps(newProps);
+ });
+
+ const elem = document.getElementById(this.tippySelectorId);
+ const opts = { content: title, appendTo: document.body };
+ if (elem && elem._tippy) elem._tippy.setProps(opts);
+ }
+
+ componentWillUnmount() {
+ setTimeout(() => {
+ const tooltip = this.tooltip[0];
+ if (tooltip) tooltip.hide();
+ }, 150);
+ }
+
+ onShow() {
+ document.addEventListener('keyup', this.handleEscapeHide);
+ }
+
+ onHide() {
+ document.removeEventListener('keyup', this.handleEscapeHide);
+ }
+
+ handleEscapeHide(e) {
+ if (this.tooltip
+ && e.keyCode === ESCAPE
+ && this.tooltip.tooltips
+ && this.tooltip.tooltips[0]) {
+ this.tooltip.tooltips[0].hide();
+ }
+ }
+
+ render() {
+ const {
+ children,
+ className,
+ title,
+ ...restProps
+ } = this.props;
+
+ let WrappedComponent;
+ let WrappedComponentBound;
+
+ if (Tooltip.buttonComponentHasButtonEmoji(children)) {
+ const { children: grandChildren } = children.props;
+
+ let otherChildren;
+
+ [WrappedComponent, ...otherChildren] = grandChildren;
+
+ WrappedComponentBound = React.cloneElement(WrappedComponent, {
+ id: this.tippySelectorId,
+ className: cx(WrappedComponent.props.className, className),
+ key: this.tippySelectorId,
+ });
+
+ const ParentComponent = React.Children.only(children);
+ const updatedChildren = [WrappedComponentBound, ...otherChildren];
+
+ return React.cloneElement(ParentComponent, null,
+ updatedChildren);
+ }
+
+ WrappedComponent = React.Children.only(children);
+
+ WrappedComponentBound = React.cloneElement(WrappedComponent, {
+ ...restProps,
+ id: this.tippySelectorId,
+ className: cx(children.props.className, className),
+ });
+
+ return WrappedComponentBound;
+ }
+}
+
+export default Tooltip;
+
+Tooltip.defaultProps = defaultProps;
+Tooltip.propTypes = propTypes;
diff --git a/src/2.7.12/imports/ui/components/common/tooltip/container.jsx b/src/2.7.12/imports/ui/components/common/tooltip/container.jsx
new file mode 100644
index 00000000..df9a29b6
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/common/tooltip/container.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
+import Tooltip from './component';
+
+const TooltipContainer = props => ;
+
+export default withTracker(() => ({
+ fullscreen: FullscreenService.getFullscreenElement(),
+}))(TooltipContainer);
diff --git a/src/2.7.12/imports/ui/components/components-data/chat-context/adapter.jsx b/src/2.7.12/imports/ui/components/components-data/chat-context/adapter.jsx
new file mode 100644
index 00000000..006795dd
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/components-data/chat-context/adapter.jsx
@@ -0,0 +1,156 @@
+import { useContext, useEffect, useState } from 'react';
+import { throttle } from '/imports/utils/throttle';
+import { ChatContext, ACTIONS, MESSAGE_TYPES } from './context';
+import { UsersContext } from '../users-context/context';
+import { makeCall } from '/imports/ui/services/api';
+import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
+import Auth from '/imports/ui/services/auth';
+import CollectionEventsBroker from '/imports/ui/services/LiveDataEventBroker/LiveDataEventBroker';
+
+let prevUserData = {};
+let currentUserData = {};
+let messageQueue = [];
+let referenceIds = {};
+
+const CHAT_CONFIG = Meteor.settings.public.chat;
+const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
+const CHAT_CLEAR_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_clear;
+const ITENS_PER_PAGE = CHAT_CONFIG.itemsPerPage;
+const TIME_BETWEEN_FETCHS = CHAT_CONFIG.timeBetweenFetchs;
+const EVENT_NAME = 'bbb-group-chat-messages-subscription-has-stoppped';
+const EVENT_NAME_SUBSCRIPTION_READY = 'bbb-group-chat-messages-subscriptions-ready';
+
+const getMessagesBeforeJoinCounter = async () => {
+ const counter = await makeCall('chatMessageBeforeJoinCounter');
+ return counter;
+};
+
+const startSyncMessagesbeforeJoin = async (dispatch) => {
+ const chatsMessagesCount = await getMessagesBeforeJoinCounter();
+ const pagesPerChat = chatsMessagesCount.map((chat) => ({
+ ...chat,
+ pages: Math.ceil(chat.count / ITENS_PER_PAGE),
+ syncedPages: 0,
+ }));
+
+ const syncRoutine = async (chatsToSync) => {
+ if (!chatsToSync.length) return;
+
+ const pagesToFetch = [...chatsToSync].sort((a, b) => a.pages - b.pages);
+ const chatWithLessPages = pagesToFetch[0];
+ chatWithLessPages.syncedPages += 1;
+ const messagesFromPage = await makeCall(
+ 'fetchMessagePerPage',
+ chatWithLessPages.chatId,
+ chatWithLessPages.syncedPages,
+ );
+
+ if (messagesFromPage.length) {
+ dispatch({
+ type: ACTIONS.ADDED,
+ value: messagesFromPage,
+ messageType: MESSAGE_TYPES.HISTORY,
+ });
+ dispatch({
+ type: ACTIONS.SYNC_STATUS,
+ value: {
+ chatId: chatWithLessPages.chatId,
+ percentage: Math.floor((chatWithLessPages.syncedPages / chatWithLessPages.pages) * 100),
+ },
+ });
+ }
+
+ await new Promise((r) => setTimeout(r, TIME_BETWEEN_FETCHS));
+ syncRoutine(pagesToFetch.filter((chat) => !(chat.syncedPages > chat.pages)));
+ };
+ syncRoutine(pagesPerChat);
+};
+
+const Adapter = () => {
+ const usingChatContext = useContext(ChatContext);
+ const { dispatch } = usingChatContext;
+ const usingUsersContext = useContext(UsersContext);
+ const { users } = usingUsersContext;
+ const [syncStarted, setSync] = useState(true);
+ const [subscriptionReady, setSubscriptionReady] = useState(false);
+ ChatLogger.trace('chatAdapter::body::users', users[Auth.meetingID]);
+
+ useEffect(() => {
+ window.addEventListener(EVENT_NAME, () => {
+ /* needed to prevent an issue with dupĺicated messages when user role is changed
+ more info: https://github.com/bigbluebutton/bigbluebutton/issues/11842 */
+ if (prevUserData.role && prevUserData?.role !== currentUserData?.role) {
+ dispatch({
+ type: ACTIONS.CLEAR_STREAM_MESSAGES,
+ });
+ }
+ });
+
+ window.addEventListener(EVENT_NAME_SUBSCRIPTION_READY, () => {
+ setSubscriptionReady(true);
+ });
+ }, []);
+
+ useEffect(() => {
+ const connectionStatus = Meteor.status();
+ if (connectionStatus.connected && !syncStarted && Auth.userID && subscriptionReady) {
+ setTimeout(() => {
+ setSync(true);
+ startSyncMessagesbeforeJoin(dispatch);
+ }, 1000);
+ }
+ }, [Meteor.status().connected, syncStarted, Auth.userID, subscriptionReady]);
+
+ /* needed to prevent an issue with dupĺicated messages when user role is changed
+ more info: https://github.com/bigbluebutton/bigbluebutton/issues/11842 */
+ useEffect(() => {
+ if (users[Auth.meetingID] && users[Auth.meetingID][Auth.userID]) {
+ if (currentUserData?.role !== users[Auth.meetingID][Auth.userID]?.role) {
+ prevUserData = currentUserData;
+ referenceIds = {};
+ }
+ currentUserData = users[Auth.meetingID][Auth.userID];
+ }
+ }, [usingUsersContext]);
+
+ useEffect(() => {
+ if (!Meteor.status().connected) return () => null;
+ setSync(false);
+ dispatch({
+ type: ACTIONS.CLEAR_ALL,
+ });
+ const throttledDispatch = throttle(
+ () => {
+ const dispatchedMessageQueue = [...messageQueue];
+ messageQueue = [];
+ dispatch({
+ type: ACTIONS.ADDED,
+ value: dispatchedMessageQueue,
+ messageType: MESSAGE_TYPES.STREAM,
+ });
+ },
+ 1000,
+ { trailing: true, leading: true },
+ );
+
+ const insertToContext = (fields) => {
+ if (fields.id === `${SYSTEM_CHAT_TYPE}-${CHAT_CLEAR_MESSAGE}`) {
+ messageQueue = [];
+ dispatch({
+ type: ACTIONS.REMOVED,
+ });
+ }
+ if (referenceIds[fields.referenceId]) return;
+
+ referenceIds[fields.referenceId] = true;
+ messageQueue.push(fields);
+ throttledDispatch();
+ };
+
+ CollectionEventsBroker.addListener('group-chat-msg', 'added', insertToContext);
+ }, [Meteor.status().connected, Meteor.connection._lastSessionId]);
+
+ return null;
+};
+
+export default Adapter;
diff --git a/src/2.7.12/imports/ui/components/components-data/chat-context/context.jsx b/src/2.7.12/imports/ui/components/components-data/chat-context/context.jsx
new file mode 100644
index 00000000..da6649db
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/components-data/chat-context/context.jsx
@@ -0,0 +1,397 @@
+import React, {
+ createContext,
+ useReducer,
+} from 'react';
+
+import Users from '/imports/api/users';
+import Auth from '/imports/ui/services/auth';
+import Storage from '/imports/ui/services/storage/session';
+import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
+import UserService from '/imports/ui/components/user-list/service';
+
+const CHAT_CONFIG = Meteor.settings.public.chat;
+const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
+const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
+const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
+const CHAT_EXPORTED_PRESENTATION_MESSAGE = CHAT_CONFIG.system_messages_keys
+ .chat_exported_presentation;
+const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result;
+const CLOSED_CHAT_LIST_KEY = 'closedChatList';
+
+export const ACTIONS = {
+ TEST: 'test',
+ ADDED: 'added',
+ CHANGED: 'changed',
+ REMOVED: 'removed',
+ LAST_READ_MESSAGE_TIMESTAMP_CHANGED: 'last_read_message_timestamp_changed',
+ INIT: 'initial_structure',
+ SYNC_STATUS: 'sync_status',
+ HAS_MESSAGE_TO_SYNC: 'has_message_to_sync',
+ CLEAR_ALL: 'clear_all',
+ CLEAR_STREAM_MESSAGES: 'clear_stream_messages',
+};
+
+export const MESSAGE_TYPES = {
+ //messages before user login, synced via makecall
+ HISTORY: 'history',
+ // messages after user login, synced via subscription
+ STREAM: 'stream',
+};
+
+export const getGroupingTime = () => Meteor.settings.public.chat.grouping_messages_window;
+export const getGroupChatId = () => Meteor.settings.public.chat.public_group_id;
+export const getLoginTime = () => (Users.findOne({ userId: Auth.userID }) || {}).authTokenValidatedTime || 0;
+
+const generateTimeWindow = (timestamp) => {
+ const groupingTime = getGroupingTime();
+ const dateInMilliseconds = Math.floor(timestamp);
+ const groupIndex = Math.floor(dateInMilliseconds / groupingTime)
+ const date = groupIndex * 30000;
+ return date;
+}
+
+export const ChatContext = createContext();
+
+const removedMessagesReadState = {};
+
+const generateStateWithNewMessage = (msg, state, msgType = MESSAGE_TYPES.HISTORY) => {
+
+ const timeWindow = generateTimeWindow(msg.timestamp);
+ const userId = msg.sender;
+ const keyName = userId + '-' + timeWindow;
+ const msgBuilder = (msg, chat) => {
+ const msgTimewindow = generateTimeWindow(msg.timestamp);
+ const key = msg.sender + '-' + msgTimewindow;
+ const chatIndex = chat?.chatIndexes[key];
+ const {
+ _id,
+ ...restMsg
+ } = msg;
+
+ const indexValue = chatIndex ? (chatIndex + 1) : 1;
+ const messageKey = key + '-' + indexValue;
+ const tempGroupMessage = {
+ [messageKey]: {
+ ...restMsg,
+ key: messageKey,
+ lastTimestamp: msg.timestamp,
+ read: msg.chatId === PUBLIC_CHAT_KEY && msg.timestamp <= getLoginTime() ? true : !!removedMessagesReadState[msg.id],
+ content: [
+ { id: msg.id, text: msg.message, time: msg.timestamp },
+ ],
+ }
+ };
+
+ return [tempGroupMessage, msg.sender, indexValue, msg.senderName];
+ };
+
+ let stateMessages = state[msg.chatId];
+
+ if (!stateMessages) {
+ if (msg.chatId === getGroupChatId()) {
+ state[msg.chatId] = {
+ count: 0,
+ chatIndexes: {},
+ preJoinMessages: {},
+ posJoinMessages: {},
+ synced: true,
+ unreadTimeWindows: new Set(),
+ unreadCount: 0,
+ };
+ } else {
+ state[msg.chatId] = {
+ count: 0,
+ lastSender: '',
+ lastSenderName: '',
+ synced: true,
+ chatIndexes: {},
+ messageGroups: {},
+ unreadTimeWindows: new Set(),
+ unreadCount: 0,
+ };
+ stateMessages = state[msg.chatId];
+ }
+
+ stateMessages = state[msg.chatId];
+ }
+
+ const forPublicChat = msg.timestamp < getLoginTime() ? stateMessages.preJoinMessages : stateMessages.posJoinMessages;
+ const forPrivateChat = stateMessages.messageGroups;
+ const messageGroups = msg.chatId === getGroupChatId() ? forPublicChat : forPrivateChat;
+ const timewindowIndex = stateMessages.chatIndexes[keyName];
+ const groupMessage = messageGroups[keyName + '-' + timewindowIndex];
+ const fromSameSender = groupMessage
+ && groupMessage.sender !== stateMessages.lastSender
+ && groupMessage.senderName !== stateMessages.lastSenderName;
+
+ if (!groupMessage || fromSameSender || msg.id.startsWith(SYSTEM_CHAT_TYPE)) {
+ const [tempGroupMessage, sender, newIndex, senderName] = msgBuilder(msg, stateMessages);
+ stateMessages.lastSender = sender;
+ stateMessages.lastSenderName = senderName;
+ stateMessages.chatIndexes[keyName] = newIndex;
+ stateMessages.lastTimewindow = keyName + '-' + newIndex;
+ ChatLogger.trace('ChatContext::formatMsg::msgBuilder::tempGroupMessage', tempGroupMessage);
+
+ const messageGroupsKeys = Object.keys(tempGroupMessage);
+ messageGroupsKeys.forEach(key => {
+ messageGroups[key] = tempGroupMessage[key];
+ const message = tempGroupMessage[key];
+ message.messageType = msgType;
+ const previousMessage = message.timestamp <= getLoginTime();
+ const amIPresenter = UserService.isUserPresenter(Auth.userID);
+ const shouldAddPresentationExportMessage = message.id
+ .includes(CHAT_EXPORTED_PRESENTATION_MESSAGE) && !amIPresenter;
+ const shouldAddPollResultMessage = message.id
+ .includes(CHAT_POLL_RESULTS_MESSAGE) && !amIPresenter;
+ if (
+ !previousMessage
+ && message.sender !== Auth.userID
+ && (!message.id.startsWith(SYSTEM_CHAT_TYPE) || shouldAddPollResultMessage
+ || shouldAddPresentationExportMessage)
+ && !message.read
+ ) {
+ stateMessages.unreadTimeWindows.add(key);
+ }
+ });
+ } else {
+ if (groupMessage) {
+ if (groupMessage.sender === stateMessages.lastSender) {
+ const previousMessage = msg.timestamp <= getLoginTime();
+ const timeWindowKey = keyName + '-' + stateMessages.chatIndexes[keyName];
+ const read = previousMessage ? true : !!removedMessagesReadState[groupMessage.id];
+
+ messageGroups[timeWindowKey] = {
+ ...groupMessage,
+ lastTimestamp: msg.timestamp,
+ read,
+ content: [
+ ...groupMessage.content,
+ { id: msg.id, text: msg.message, time: msg.timestamp }
+ ],
+ };
+ if (!read && groupMessage.sender !== Auth.userID) {
+ stateMessages.unreadTimeWindows.add(timeWindowKey);
+ }
+ }
+ }
+ }
+
+ return state;
+}
+
+const reducer = (state, action) => {
+ switch (action.type) {
+ case ACTIONS.TEST: {
+ ChatLogger.debug(ACTIONS.TEST);
+ return {
+ ...state,
+ ...action.value,
+ };
+ }
+ case ACTIONS.ADDED: {
+ ChatLogger.debug(ACTIONS.ADDED);
+
+ const batchMsgs = action.value;
+ const closedChatsToOpen = new Set();
+ const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
+ const newState = batchMsgs.reduce((acc, i) => {
+ const message = i;
+ const chatId = message.chatId;
+ const chatClosedTimestamp = currentClosedChats.find(closedChat => closedChat.chatId === chatId)?.timestamp;
+ if (
+ chatId !== PUBLIC_GROUP_CHAT_KEY
+ && chatClosedTimestamp
+ && message.timestamp > chatClosedTimestamp ) {
+ closedChatsToOpen.add(chatId)
+ }
+ return generateStateWithNewMessage(message, acc, action.messageType);
+ }, state);
+
+ if (closedChatsToOpen.size) {
+ const closedChats = currentClosedChats.filter(closedChat => !closedChatsToOpen.has(closedChat.chatId));
+ Storage.setItem(CLOSED_CHAT_LIST_KEY, closedChats);
+ }
+ // const newState = generateStateWithNewMessage(action.value, state);
+ return { ...newState };
+ }
+ case ACTIONS.CHANGED: {
+ return {
+ ...state,
+ ...action.value,
+ };
+ }
+ case ACTIONS.REMOVED: {
+ ChatLogger.debug(ACTIONS.REMOVED);
+ if (state[PUBLIC_GROUP_CHAT_KEY]) {
+ state[PUBLIC_GROUP_CHAT_KEY] = {
+ count: 0,
+ lastSender: '',
+ chatIndexes: {},
+ syncing: false,
+ preJoinMessages: {},
+ posJoinMessages: {},
+ unreadTimeWindows: new Set(),
+ unreadCount: 0,
+ };
+ }
+ return state;
+ }
+ case ACTIONS.LAST_READ_MESSAGE_TIMESTAMP_CHANGED: {
+ ChatLogger.debug(ACTIONS.LAST_READ_MESSAGE_TIMESTAMP_CHANGED);
+ const { timestamp, chatId } = action.value;
+ const newState = {
+ ...state,
+ };
+ const selectedChatId = chatId === PUBLIC_CHAT_KEY ? PUBLIC_GROUP_CHAT_KEY : chatId;
+ const chat = state[selectedChatId];
+ if (!chat) return state;
+ ['posJoinMessages', 'preJoinMessages', 'messageGroups'].forEach(messageGroupName => {
+ const messageGroup = chat[messageGroupName];
+ if (messageGroup) {
+ const timeWindowsids = Object.keys(messageGroup);
+ timeWindowsids.forEach(timeWindowId => {
+ const timeWindow = messageGroup[timeWindowId];
+ if (timeWindow) {
+ if (!timeWindow.read) {
+ if (timeWindow.lastTimestamp <= timestamp) {
+ newState[selectedChatId].unreadTimeWindows.delete(timeWindowId);
+
+ newState[selectedChatId][messageGroupName][timeWindowId] = {
+ ...timeWindow,
+ read: true,
+ };
+
+
+ newState[selectedChatId] = {
+ ...newState[selectedChatId],
+ };
+ newState[selectedChatId][messageGroupName] = {
+ ...newState[selectedChatId][messageGroupName],
+ };
+ newState[chatId === PUBLIC_CHAT_KEY ? PUBLIC_GROUP_CHAT_KEY : chatId][messageGroupName][timeWindowId] = {
+ ...newState[selectedChatId][messageGroupName][timeWindowId],
+ };
+ }
+ }
+ }
+ });
+ }
+ });
+ return newState;
+ }
+ case ACTIONS.INIT: {
+ ChatLogger.debug(ACTIONS.INIT);
+ const { chatId } = action;
+ const newState = { ...state };
+
+ if (!newState[chatId]) {
+ newState[chatId] = {
+ count: 0,
+ lastSender: '',
+ chatIndexes: {},
+ messageGroups: {},
+ unreadTimeWindows: new Set(),
+ unreadCount: 0,
+ };
+ }
+ return state;
+ }
+ case ACTIONS.SYNC_STATUS: {
+ ChatLogger.debug(ACTIONS.SYNC_STATUS);
+ const newState = { ...state };
+ newState[action.value.chatId].syncedPercent = action.value.percentage;
+ newState[action.value.chatId].syncing = action.value.percentage < 100 ? true : false;
+
+ return newState;
+ }
+ case ACTIONS.CLEAR_ALL: {
+ ChatLogger.debug(ACTIONS.CLEAR_ALL);
+ const newState = { ...state };
+ const chatIds = Object.keys(newState);
+ chatIds.forEach((chatId) => {
+ newState[chatId] = chatId === PUBLIC_GROUP_CHAT_KEY ?
+ {
+ count: 0,
+ lastSender: '',
+ chatIndexes: {},
+ preJoinMessages: {},
+ posJoinMessages: {},
+ syncing: false,
+ syncedPercent: 0,
+ unreadTimeWindows: new Set(),
+ unreadCount: 0,
+ }
+ :
+ {
+ count: 0,
+ lastSender: '',
+ chatIndexes: {},
+ messageGroups: {},
+ syncing: false,
+ syncedPercent: 0,
+ unreadTimeWindows: new Set(),
+ unreadCount: 0,
+ };
+ });
+ return newState;
+ }
+ // BBB don't remove individual messages, so when a message is removed it means the chat is cleared ( by admin, or for resync )
+ // considering it, we remove all messages from all chats
+ case ACTIONS.CLEAR_STREAM_MESSAGES: {
+ ChatLogger.debug(ACTIONS.CLEAR_STREAM_MESSAGES);
+ const newState = { ...state };
+ const chatIds = Object.keys(newState);
+ chatIds.forEach((chatId) => {
+ const chat = newState[chatId];
+ ['posJoinMessages', 'messageGroups'].forEach((group) => {
+ const messages = chat[group];
+ if (messages) {
+ const timeWindowIds = Object.keys(messages);
+ timeWindowIds.forEach((timeWindowId) => {
+ const timeWindow = messages[timeWindowId];
+ if (timeWindow.messageType === MESSAGE_TYPES.STREAM) {
+ chat.unreadTimeWindows.delete(timeWindowId);
+ removedMessagesReadState[newState[chatId][group][timeWindowId].id] = newState[chatId][group][timeWindowId].read;
+ delete newState[chatId][group][timeWindowId];
+ }
+ });
+ }
+ })
+ });
+
+ return newState;
+ }
+ default: {
+ throw new Error(`Unexpected action: ${JSON.stringify(action)}`);
+ }
+ }
+};
+
+export const ChatContextProvider = (props) => {
+ const [chatContextState, chatContextDispatch] = useReducer(reducer, {});
+ ChatLogger.debug('dispatch', chatContextDispatch);
+ return (
+
+ {props.children}
+
+ );
+}
+
+
+export const ContextConsumer = Component => props => (
+
+ {contexts => }
+
+);
+
+export default {
+ ContextConsumer,
+ ChatContextProvider,
+}
diff --git a/src/2.7.12/imports/ui/components/components-data/group-chat-context/adapter.jsx b/src/2.7.12/imports/ui/components/components-data/group-chat-context/adapter.jsx
new file mode 100644
index 00000000..13644aae
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/components-data/group-chat-context/adapter.jsx
@@ -0,0 +1,25 @@
+import { useContext, useEffect } from 'react';
+import GroupChat from '/imports/api/group-chat';
+import { GroupChatContext, ACTIONS } from './context';
+
+const Adapter = () => {
+ const usingGroupChatContext = useContext(GroupChatContext);
+ const { dispatch } = usingGroupChatContext;
+
+ useEffect(() => {
+ const groupChatCursor = GroupChat.find({});
+
+ groupChatCursor.observe({
+ added: (obj) => {
+ dispatch({
+ type: ACTIONS.ADDED,
+ value: {
+ groupChat: obj,
+ },
+ });
+ },
+ });
+ }, []);
+};
+
+export default Adapter;
diff --git a/src/2.7.12/imports/ui/components/components-data/group-chat-context/context.jsx b/src/2.7.12/imports/ui/components/components-data/group-chat-context/context.jsx
new file mode 100644
index 00000000..7fe3f4e9
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/components-data/group-chat-context/context.jsx
@@ -0,0 +1,76 @@
+import React, {
+ createContext,
+ useReducer,
+} from 'react';
+import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
+
+export const ACTIONS = {
+ TEST: 'test',
+ ADDED: 'added',
+ CHANGED: 'changed',
+ REMOVED: 'removed',
+};
+
+export const GroupChatContext = createContext();
+
+const reducer = (state, action) => {
+ switch (action.type) {
+ case ACTIONS.TEST: {
+ return {
+ ...state,
+ ...action.value,
+ };
+ }
+
+ case ACTIONS.ADDED:
+ case ACTIONS.CHANGED: {
+ ChatLogger.debug('GroupChatContextProvider::reducer::added', { ...action });
+ const { groupChat } = action.value;
+
+ const newState = {
+ ...state,
+ [groupChat.chatId]: {
+ ...groupChat,
+ },
+ };
+ return newState;
+ }
+
+ case ACTIONS.REMOVED: {
+ ChatLogger.debug('GroupChatContextProvider::reducer::removed', { ...action });
+
+ return state;
+ }
+ default: {
+ throw new Error(`Unexpected action: ${JSON.stringify(action)}`);
+ }
+ }
+};
+
+export const GroupChatContextProvider = (props) => {
+ const [groupChatContextState, groupChatContextDispatch] = useReducer(reducer, {});
+ ChatLogger.debug('UsersContextProvider::groupChatContextState', groupChatContextState);
+ return (
+
+ {props.children}
+
+ );
+};
+
+export const GroupChatContextConsumer = Component => props => (
+
+ {contexts => }
+
+);
+
+export default {
+ GroupChatContextConsumer,
+ GroupChatContextProvider,
+};
diff --git a/src/2.7.12/imports/ui/components/components-data/users-context/adapter.jsx b/src/2.7.12/imports/ui/components/components-data/users-context/adapter.jsx
new file mode 100644
index 00000000..4953f2e7
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/components-data/users-context/adapter.jsx
@@ -0,0 +1,84 @@
+import { useContext, useEffect } from 'react';
+import { CurrentUser } from '/imports/api/users';
+import Users from '/imports/api/users';
+import UsersPersistentData from '/imports/api/users-persistent-data';
+import { UsersContext, ACTIONS } from './context';
+import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
+
+const Adapter = () => {
+ const usingUsersContext = useContext(UsersContext);
+ const { dispatch } = usingUsersContext;
+
+ useEffect(() => {
+ const usersPersistentDataCursor = UsersPersistentData.find({}, { sort: { timestamp: 1 } });
+ usersPersistentDataCursor.observe({
+ added: (obj) => {
+ ChatLogger.debug('usersAdapter::observe::added_persistent_user', obj);
+ dispatch({
+ type: ACTIONS.ADDED_USER_PERSISTENT_DATA,
+ value: {
+ user: obj,
+ },
+ });
+ },
+ changed: (obj) => {
+ ChatLogger.debug('usersAdapter::observe::changed_persistent_user', obj);
+ dispatch({
+ type: ACTIONS.CHANGED_USER_PERSISTENT_DATA,
+ value: {
+ user: obj,
+ },
+ });
+ },
+ removed: (obj) => {
+ ChatLogger.debug('usersAdapter::observe::removed', obj);
+ dispatch({
+ type: ACTIONS.REMOVED,
+ value: {
+ user: obj,
+ },
+ });
+ },
+ });
+ }, []);
+
+ useEffect(() => {
+ const usersCursor = Users.find({}, { sort: { timestamp: 1 } });
+ const CurrentUserCursor = CurrentUser.find({});
+ usersCursor.observe({
+ added: (obj) => {
+ ChatLogger.debug('usersAdapter::observe::added', obj);
+ dispatch({
+ type: ACTIONS.ADDED,
+ value: {
+ user: obj,
+ },
+ });
+ },
+ changed: (obj) => {
+ dispatch({
+ type: ACTIONS.CHANGED,
+ value: {
+ user: obj,
+ },
+ });
+ },
+ });
+
+ CurrentUserCursor.observe({
+ added: (obj) => {
+ ChatLogger.debug('usersAdapter::observe::current-user::added', obj);
+ dispatch({
+ type: ACTIONS.ADDED,
+ value: {
+ user: obj,
+ },
+ });
+ },
+ });
+ }, []);
+
+ return null;
+};
+
+export default Adapter;
diff --git a/src/2.7.12/imports/ui/components/components-data/users-context/context.jsx b/src/2.7.12/imports/ui/components/components-data/users-context/context.jsx
new file mode 100644
index 00000000..b9722e2b
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/components-data/users-context/context.jsx
@@ -0,0 +1,133 @@
+import React, {
+ createContext,
+ useReducer,
+} from 'react';
+import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
+
+export const ACTIONS = {
+ TEST: 'test',
+ ADDED: 'added',
+ CHANGED: 'changed',
+ REMOVED: 'removed',
+ ADDED_USER_PERSISTENT_DATA: 'added_user_persistent_data',
+ CHANGED_USER_PERSISTENT_DATA: 'changed_user_persistent_data',
+};
+
+export const UsersContext = createContext();
+
+const reducer = (state, action) => {
+ switch (action.type) {
+ case ACTIONS.TEST: {
+ return {
+ ...state,
+ ...action.value,
+ };
+ }
+
+ case ACTIONS.ADDED:
+ case ACTIONS.CHANGED: {
+ ChatLogger.debug('UsersContextProvider::reducer::added', { ...action });
+ const { user } = action.value;
+
+ const newState = { ...state };
+
+ if (!newState[user.meetingId]) {
+ newState[user.meetingId] = {};
+ }
+ newState[user.meetingId][user.userId] = {
+ ...user,
+ };
+ return newState;
+ }
+ case ACTIONS.REMOVED: {
+ ChatLogger.debug('UsersContextProvider::reducer::removed', { ...action });
+
+ const { user } = action.value;
+ const stateUser = state[user.meetingId][user.userId];
+ if (stateUser) {
+ const newState = { ...state };
+ newState[user.meetingId][user.userId] = {
+ ...stateUser,
+ loggedOut: true,
+ };
+
+ return newState;
+ }
+ return state;
+ }
+
+ // USER PERSISTENT DATA
+ case ACTIONS.ADDED_USER_PERSISTENT_DATA: {
+ const { user } = action.value;
+ if (state[user.meetingId] && state[user.meetingId][user.userId]) {
+ if (state[user.meetingId][user.userId].loggedOut) {
+ const newState = { ...state };
+ newState[user.meetingId][user.userId] = {
+ ...state[user.meetingId][user.userId],
+ loggedOut: false,
+ };
+
+ return newState;
+ }
+ return state;
+ }
+
+ const newState = { ...state };
+
+ if (!newState[user.meetingId]) {
+ newState[user.meetingId] = {};
+ }
+ newState[user.meetingId][user.userId] = {
+ ...user,
+ };
+ return newState;
+ }
+ case ACTIONS.CHANGED_USER_PERSISTENT_DATA: {
+ const { user } = action.value;
+ const stateUser = state[user.meetingId][user.userId];
+ if (stateUser) {
+ const newState = { ...state };
+ newState[user.meetingId][user.userId] = {
+ ...stateUser,
+ ...user,
+ };
+
+ return newState;
+ }
+ return state;
+ }
+ default: {
+ throw new Error(`Unexpected action: ${JSON.stringify(action)}`);
+ }
+ }
+};
+
+export const UsersContextProvider = (props) => {
+ const [usersContextState, usersContextDispatch] = useReducer(reducer, {});
+ ChatLogger.debug('UsersContextProvider::usersContextState', usersContextState);
+ return (
+
+ {props.children}
+
+ );
+};
+
+export const UsersContextConsumer = Component => props => (
+
+ {contexts => }
+
+);
+
+export const withUsersConsumer = Component => UsersContextConsumer(Component);
+
+export default {
+ UsersContextConsumer,
+ UsersContextProvider,
+};
diff --git a/src/2.7.12/imports/ui/components/components-data/users-context/service.js b/src/2.7.12/imports/ui/components/components-data/users-context/service.js
new file mode 100644
index 00000000..dc0512dd
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/components-data/users-context/service.js
@@ -0,0 +1,31 @@
+import {
+ useState, useContext, useRef, useEffect,
+} from 'react';
+import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
+import { throttle } from '/imports/utils/throttle';
+
+const USER_JOIN_UPDATE_THROTTLE_TIME = 1000;
+
+export default function useContextUsers() {
+ const usingUsersContext = useContext(UsersContext);
+ const { users: contextUsers } = usingUsersContext;
+
+ const [users, setUsers] = useState(null);
+ const [isReady, setIsReady] = useState(true);
+
+ const throttledSetUsers = useRef(throttle(() => {
+ setUsers(contextUsers);
+ setIsReady(true);
+ },
+ USER_JOIN_UPDATE_THROTTLE_TIME, { trailing: true }));
+
+ useEffect(() => {
+ setIsReady(false);
+ throttledSetUsers.current();
+ }, [contextUsers]);
+
+ return {
+ users,
+ isReady,
+ };
+}
diff --git a/src/2.7.12/imports/ui/components/connection-status/button/component.jsx b/src/2.7.12/imports/ui/components/connection-status/button/component.jsx
new file mode 100644
index 00000000..21058847
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/button/component.jsx
@@ -0,0 +1,123 @@
+import React, { PureComponent } from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import Button from '/imports/ui/components/common/button/component';
+import ConnectionStatusModalContainer from '/imports/ui/components/connection-status/modal/container';
+import ConnectionStatusService from '/imports/ui/components/connection-status/service';
+import Icon from '/imports/ui/components/connection-status/icon/component';
+import Styled from './styles';
+
+const intlMessages = defineMessages({
+ label: {
+ id: 'app.connection-status.label',
+ description: 'Connection status button label',
+ },
+ description: {
+ id: 'app.connection-status.description',
+ description: 'Connection status button description',
+ },
+});
+
+class ConnectionStatusButton extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isModalOpen: false,
+ }
+ }
+
+ renderIcon(level = 'normal') {
+ return(
+
+
+
+ );
+ }
+
+ setModalIsOpen = (isOpen) => this.setState({ isModalOpen: isOpen });
+
+ renderModal(isModalOpen) {
+ return (
+ isModalOpen ?
+ : null
+ )
+ }
+
+ render() {
+ const {
+ intl,
+ connected,
+ } = this.props;
+ const { isModalOpen } = this.state;
+
+
+ if (!connected) {
+ return (
+
+ {}}
+ data-test="connectionStatusButton"
+ />
+ {this.renderModal(isModalOpen)}
+
+ );
+ }
+
+ const {
+ stats,
+ } = this.props;
+
+ let color;
+ switch (stats) {
+ case 'warning':
+ color = 'success';
+ break;
+ case 'danger':
+ color = 'warning';
+ ConnectionStatusService.notification('warning', intl);
+ break;
+ case 'critical':
+ color = 'danger';
+ ConnectionStatusService.notification('error', intl);
+ break;
+ default:
+ color = 'success';
+ }
+
+ const currentStatus = stats ? stats : 'normal';
+
+ return (
+
+ this.setState({isModalOpen: true})}
+ data-test="connectionStatusButton"
+ />
+ {this.renderModal(isModalOpen)}
+
+ );
+ }
+}
+
+export default injectIntl(ConnectionStatusButton);
diff --git a/src/2.7.12/imports/ui/components/connection-status/button/container.jsx b/src/2.7.12/imports/ui/components/connection-status/button/container.jsx
new file mode 100644
index 00000000..50b42475
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/button/container.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Meteor } from 'meteor/meteor';
+import { withTracker } from 'meteor/react-meteor-data';
+import ConnectionStatusService from '../service';
+import ConnectionStatusButtonComponent from './component';
+
+const connectionStatusButtonContainer = props => ;
+
+export default withTracker(() => {
+ const { connected } = Meteor.status();
+
+ return {
+ connected,
+ stats: ConnectionStatusService.getStats(),
+ };
+})(connectionStatusButtonContainer);
diff --git a/src/2.7.12/imports/ui/components/connection-status/button/styles.js b/src/2.7.12/imports/ui/components/connection-status/button/styles.js
new file mode 100644
index 00000000..0b70eab9
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/button/styles.js
@@ -0,0 +1,15 @@
+import styled from 'styled-components';
+
+const IconWrapper = styled.div`
+ width: 1.025rem;
+ height: 1.025rem;
+`;
+
+const ButtonWrapper = styled.div`
+ margin: 0 .5rem;
+`;
+
+export default {
+ IconWrapper,
+ ButtonWrapper,
+};
diff --git a/src/2.7.12/imports/ui/components/connection-status/icon/component.jsx b/src/2.7.12/imports/ui/components/connection-status/icon/component.jsx
new file mode 100644
index 00000000..851e7e7b
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/icon/component.jsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import Styled from './styles';
+
+const STATS = {
+ critical: {
+ bars: 1,
+ },
+ danger: {
+ bars: 2,
+ },
+ warning: {
+ bars: 3,
+ },
+ normal: {
+ bars: 4,
+ },
+};
+
+const Icon = ({ level, grayscale }) => (
+ <>
+
+
+ = 2} />
+ = 3} />
+ = 4} />
+
+ >
+);
+
+export default Icon;
diff --git a/src/2.7.12/imports/ui/components/connection-status/icon/styles.js b/src/2.7.12/imports/ui/components/connection-status/icon/styles.js
new file mode 100644
index 00000000..81714a51
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/icon/styles.js
@@ -0,0 +1,86 @@
+import styled from 'styled-components';
+import {
+ colorWhite,
+ colorDanger,
+ colorWarning,
+ colorSuccess,
+} from '/imports/ui/stylesheets/styled-components/palette';
+
+const SignalBars = styled.div`
+ align-items: flex-end;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ height: 100%;
+
+ ${({ grayscale }) => grayscale && `
+ & > div {
+ background-color: ${colorWhite};
+ }
+ `}
+
+ ${({ grayscale, level }) => !grayscale && level === 'critical' && `
+ & > div {
+ background-color: ${colorDanger};
+ }
+ `}
+
+ ${({ grayscale, level }) => !grayscale && level === 'danger' && `
+ & > div {
+ background-color: ${colorWarning};
+ }
+ `}
+
+ ${({ grayscale, level }) => !grayscale && level === 'warning' && `
+ & > div {
+ background-color: ${colorSuccess};
+ }
+ `}
+
+ ${({ grayscale, level }) => !grayscale && level === 'normal' && `
+ & > div {
+ background-color: ${colorWhite};
+ }
+ `}
+`;
+
+const Bar = styled.div`
+ width: 20%;
+ border-radius: .46875em;
+`;
+
+const FirstBar = styled(Bar)`
+ height: 25%;
+`;
+
+const SecondBar = styled(Bar)`
+ height: 50%;
+
+ ${({ active }) => !active && `
+ opacity: .5;
+ `}
+`;
+
+const ThirdBar = styled(Bar)`
+ height: 75%;
+
+ ${({ active }) => !active && `
+ opacity: .5;
+ `}
+`;
+
+const FourthBar = styled(Bar)`
+ height: 100%;
+
+ ${({ active }) => !active && `
+ opacity: .5;
+ `}
+`;
+
+export default {
+ SignalBars,
+ FirstBar,
+ SecondBar,
+ ThirdBar,
+ FourthBar,
+};
diff --git a/src/2.7.12/imports/ui/components/connection-status/modal/component.jsx b/src/2.7.12/imports/ui/components/connection-status/modal/component.jsx
new file mode 100644
index 00000000..5dfd87fc
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/modal/component.jsx
@@ -0,0 +1,588 @@
+import React, { PureComponent } from 'react';
+import { FormattedTime, defineMessages, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import UserAvatar from '/imports/ui/components/user-avatar/component';
+import Icon from '/imports/ui/components/connection-status/icon/component';
+import Service from '../service';
+import Styled from './styles';
+import ConnectionStatusHelper from '../status-helper/container';
+
+const NETWORK_MONITORING_INTERVAL_MS = 2000;
+const MIN_TIMEOUT = 3000;
+
+const intlMessages = defineMessages({
+ ariaTitle: {
+ id: 'app.connection-status.ariaTitle',
+ description: 'Connection status aria title',
+ },
+ title: {
+ id: 'app.connection-status.title',
+ description: 'Connection status title',
+ },
+ description: {
+ id: 'app.connection-status.description',
+ description: 'Connection status description',
+ },
+ empty: {
+ id: 'app.connection-status.empty',
+ description: 'Connection status empty',
+ },
+ more: {
+ id: 'app.connection-status.more',
+ description: 'More about conectivity issues',
+ },
+ audioLabel: {
+ id: 'app.settings.audioTab.label',
+ description: 'Audio label',
+ },
+ videoLabel: {
+ id: 'app.settings.videoTab.label',
+ description: 'Video label',
+ },
+ copy: {
+ id: 'app.connection-status.copy',
+ description: 'Copy network data',
+ },
+ copied: {
+ id: 'app.connection-status.copied',
+ description: 'Copied network data',
+ },
+ offline: {
+ id: 'app.connection-status.offline',
+ description: 'Offline user',
+ },
+ dataSaving: {
+ id: 'app.settings.dataSavingTab.description',
+ description: 'Description of data saving',
+ },
+ webcam: {
+ id: 'app.settings.dataSavingTab.webcam',
+ description: 'Webcam data saving switch',
+ },
+ screenshare: {
+ id: 'app.settings.dataSavingTab.screenShare',
+ description: 'Screenshare data saving switch',
+ },
+ on: {
+ id: 'app.switch.onLabel',
+ description: 'label for toggle switch on state',
+ },
+ off: {
+ id: 'app.switch.offLabel',
+ description: 'label for toggle switch off state',
+ },
+ no: {
+ id: 'app.connection-status.no',
+ description: 'No to is using turn',
+ },
+ yes: {
+ id: 'app.connection-status.yes',
+ description: 'Yes to is using turn',
+ },
+ usingTurn: {
+ id: 'app.connection-status.usingTurn',
+ description: 'User is using turn server',
+ },
+ jitter: {
+ id: 'app.connection-status.jitter',
+ description: 'Jitter buffer in ms',
+ },
+ lostPackets: {
+ id: 'app.connection-status.lostPackets',
+ description: 'Number of lost packets',
+ },
+ audioUploadRate: {
+ id: 'app.connection-status.audioUploadRate',
+ description: 'Label for audio current upload rate',
+ },
+ audioDownloadRate: {
+ id: 'app.connection-status.audioDownloadRate',
+ description: 'Label for audio current download rate',
+ },
+ videoUploadRate: {
+ id: 'app.connection-status.videoUploadRate',
+ description: 'Label for video current upload rate',
+ },
+ videoDownloadRate: {
+ id: 'app.connection-status.videoDownloadRate',
+ description: 'Label for video current download rate',
+ },
+ connectionStats: {
+ id: 'app.connection-status.connectionStats',
+ description: 'Label for Connection Stats tab',
+ },
+ myLogs: {
+ id: 'app.connection-status.myLogs',
+ description: 'Label for My Logs tab',
+ },
+ sessionLogs: {
+ id: 'app.connection-status.sessionLogs',
+ description: 'Label for Session Logs tab',
+ },
+ next: {
+ id: 'app.connection-status.next',
+ description: 'Label for the next page of the connection stats tab',
+ },
+ prev: {
+ id: 'app.connection-status.prev',
+ description: 'Label for the previous page of the connection stats tab',
+ },
+ clientNotResponding: {
+ id: 'app.connection-status.clientNotRespondingWarning',
+ description: 'Text for Client not responding warning',
+ },
+});
+
+const propTypes = {
+ setModalIsOpen: PropTypes.func.isRequired,
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func.isRequired,
+ }).isRequired,
+};
+
+const isConnectionStatusEmpty = (connectionStatus) => {
+ // Check if it's defined
+ if (!connectionStatus) return true;
+
+ // Check if it's an array
+ if (!Array.isArray(connectionStatus)) return true;
+
+ // Check if is empty
+ if (connectionStatus.length === 0) return true;
+
+ return false;
+};
+
+class ConnectionStatusComponent extends PureComponent {
+ constructor(props) {
+ super(props);
+
+ const { intl } = this.props;
+
+ this.help = Service.getHelp();
+ this.state = {
+ selectedTab: 0,
+ hasNetworkData: false,
+ copyButtonText: intl.formatMessage(intlMessages.copy),
+ networkData: {
+ user: {
+
+ },
+ audio: {
+ audioCurrentUploadRate: 0,
+ audioCurrentDownloadRate: 0,
+ jitter: 0,
+ packetsLost: 0,
+ transportStats: {},
+ },
+ video: {
+ videoCurrentUploadRate: 0,
+ videoCurrentDownloadRate: 0,
+ },
+ },
+ };
+ this.setButtonMessage = this.setButtonMessage.bind(this);
+ this.rateInterval = null;
+ this.audioUploadLabel = intl.formatMessage(intlMessages.audioUploadRate);
+ this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate);
+ this.videoUploadLabel = intl.formatMessage(intlMessages.videoUploadRate);
+ this.videoDownloadLabel = intl.formatMessage(intlMessages.videoDownloadRate);
+ this.handleSelectTab = this.handleSelectTab.bind(this);
+ }
+
+ async componentDidMount() {
+ this.startMonitoringNetwork();
+ }
+
+ componentWillUnmount() {
+ Meteor.clearInterval(this.rateInterval);
+ }
+
+ handleSelectTab(tab) {
+ this.setState({
+ selectedTab: tab,
+ });
+ }
+
+ setButtonMessage(msg) {
+ this.setState({
+ copyButtonText: msg,
+ });
+ }
+
+ /**
+ * Start monitoring the network data.
+ * @return {Promise} A Promise that resolves when process started.
+ */
+ async startMonitoringNetwork() {
+ let previousData = await Service.getNetworkData();
+ this.rateInterval = Meteor.setInterval(async () => {
+ const data = await Service.getNetworkData();
+
+ const {
+ outbound: audioCurrentUploadRate,
+ inbound: audioCurrentDownloadRate,
+ } = Service.calculateBitsPerSecond(data.audio, previousData.audio);
+
+ const inboundRtp = Service.getDataType(data.audio, 'inbound-rtp')[0];
+
+ const jitter = inboundRtp
+ ? inboundRtp.jitterBufferAverage
+ : 0;
+
+ const packetsLost = inboundRtp
+ ? inboundRtp.packetsLost
+ : 0;
+
+ const audio = {
+ audioCurrentUploadRate,
+ audioCurrentDownloadRate,
+ jitter,
+ packetsLost,
+ transportStats: data.audio.transportStats,
+ };
+
+ const {
+ outbound: videoCurrentUploadRate,
+ inbound: videoCurrentDownloadRate,
+ } = Service.calculateBitsPerSecondFromMultipleData(data.video,
+ previousData.video);
+
+ const video = {
+ videoCurrentUploadRate,
+ videoCurrentDownloadRate,
+ };
+
+ const { user } = data;
+
+ const networkData = {
+ user,
+ audio,
+ video,
+ };
+
+ previousData = data;
+ this.setState({
+ networkData,
+ hasNetworkData: true,
+ });
+ }, NETWORK_MONITORING_INTERVAL_MS);
+ }
+
+ /**
+ * Copy network data to clipboard
+ * @return {Promise} A Promise that is resolved after data is copied.
+ *
+ *
+ */
+ async copyNetworkData() {
+ const { intl } = this.props;
+ const {
+ networkData,
+ hasNetworkData,
+ } = this.state;
+
+ if (!hasNetworkData) return;
+
+ this.setButtonMessage(intl.formatMessage(intlMessages.copied));
+
+ const data = JSON.stringify(networkData, null, 2);
+
+ await navigator.clipboard.writeText(data);
+
+ this.copyNetworkDataTimeout = setTimeout(() => {
+ this.setButtonMessage(intl.formatMessage(intlMessages.copy));
+ }, MIN_TIMEOUT);
+ }
+
+ renderEmpty() {
+ const { intl } = this.props;
+
+ return (
+
+
+
+
+ {intl.formatMessage(intlMessages.empty)}
+
+
+
+
+ );
+ }
+
+ renderConnections() {
+ const {
+ connectionStatus,
+ intl,
+ } = this.props;
+
+ const { selectedTab } = this.state;
+
+ if (isConnectionStatusEmpty(connectionStatus)) return this.renderEmpty();
+
+ let connections = connectionStatus;
+ if (selectedTab === 1) {
+ connections = connections.filter(conn => conn.you);
+ if (isConnectionStatusEmpty(connections)) return this.renderEmpty();
+ }
+
+ return connections.map((conn, index) => {
+ const dateTime = new Date(conn.timestamp);
+ return (
+
+
+
+
+ {conn.name.toLowerCase().slice(0, 2)}
+
+
+
+
+
+ {conn.name}
+ {conn.offline ? ` (${intl.formatMessage(intlMessages.offline)})` : null}
+
+
+
+
+
+
+
+ { conn.notResponding && !conn.offline
+ ? (
+
+ {intl.formatMessage(intlMessages.clientNotResponding)}
+
+ ) : null }
+
+
+
+ { conn.timestamp ?
+
+
+
+ : null
+ }
+
+
+
+ );
+ });
+ }
+
+ /**
+ * Render network data , containing information abount current upload and
+ * download rates
+ * @return {Object} The component to be renderized.
+ */
+ renderNetworkData() {
+ const { enableNetworkStats } = Meteor.settings.public.app;
+
+ if (!enableNetworkStats) {
+ return null;
+ }
+
+ const {
+ audioUploadLabel,
+ audioDownloadLabel,
+ videoUploadLabel,
+ videoDownloadLabel,
+ } = this;
+
+ const { intl, setModalIsOpen } = this.props;
+
+ const { networkData } = this.state;
+
+ const {
+ audioCurrentUploadRate,
+ audioCurrentDownloadRate,
+ jitter,
+ packetsLost,
+ transportStats,
+ } = networkData.audio;
+
+ const {
+ videoCurrentUploadRate,
+ videoCurrentDownloadRate,
+ } = networkData.video;
+
+ let isUsingTurn = '--';
+
+ if (transportStats) {
+ switch (transportStats.isUsingTurn) {
+ case true:
+ isUsingTurn = intl.formatMessage(intlMessages.yes);
+ break;
+ case false:
+ isUsingTurn = intl.formatMessage(intlMessages.no);
+ break;
+ default:
+ break;
+ }
+ }
+
+ return (
+
+
+
+ setModalIsOpen(false)} />
+
+
+
+
+
+ {`${audioUploadLabel}`}
+ {`${audioCurrentUploadRate}k ↑`}
+
+
+ {`${videoUploadLabel}`}
+ {`${videoCurrentUploadRate}k ↑`}
+
+
+ {`${intl.formatMessage(intlMessages.jitter)}`}
+ {`${jitter} ms`}
+
+
+ {`${intl.formatMessage(intlMessages.usingTurn)}`}
+ {`${isUsingTurn}`}
+
+
+
+
+
+ {`${audioDownloadLabel}`}
+ {`${audioCurrentDownloadRate}k ↓`}
+
+
+ {`${videoDownloadLabel}`}
+ {`${videoCurrentDownloadRate}k ↓`}
+
+
+ {`${intl.formatMessage(intlMessages.lostPackets)}`}
+ {`${packetsLost}`}
+
+
+ Content Hidden
+ 0
+
+
+
+
+ );
+ }
+
+ /**
+ * Renders the clipboard's copy button, for network stats.
+ * @return {Object} - The component to be renderized
+ */
+ renderCopyDataButton() {
+ const { enableCopyNetworkStatsButton } = Meteor.settings.public.app;
+
+ if (!enableCopyNetworkStatsButton) {
+ return null;
+ }
+
+ const { hasNetworkData, copyButtonText } = this.state;
+ return (
+
+
+ {copyButtonText}
+
+
+ );
+ }
+
+ render() {
+ const {
+ setModalIsOpen,
+ intl,
+ isModalOpen,
+ } = this.props;
+
+ const { selectedTab } = this.state;
+
+ return (
+ setModalIsOpen(false)}
+ setIsOpen={setModalIsOpen}
+ hideBorder
+ isOpen={isModalOpen}
+ contentLabel={intl.formatMessage(intlMessages.ariaTitle)}
+ data-test="connectionStatusModal"
+ >
+
+
+
+ {intl.formatMessage(intlMessages.title)}
+
+
+
+
+
+
+ {intl.formatMessage(intlMessages.title)}
+
+
+ {intl.formatMessage(intlMessages.myLogs)}
+
+ {Service.isModerator()
+ && (
+
+ {intl.formatMessage(intlMessages.sessionLogs)}
+
+ )
+ }
+
+
+
+ {this.renderNetworkData()}
+ {this.renderCopyDataButton()}
+
+
+
+ {this.renderConnections()}
+
+ {Service.isModerator()
+ && (
+
+ {this.renderConnections()}
+
+ )
+ }
+
+
+
+ );
+ }
+}
+
+ConnectionStatusComponent.propTypes = propTypes;
+
+export default injectIntl(ConnectionStatusComponent);
diff --git a/src/2.7.12/imports/ui/components/connection-status/modal/container.jsx b/src/2.7.12/imports/ui/components/connection-status/modal/container.jsx
new file mode 100644
index 00000000..b4559ac7
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/modal/container.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import ConnectionStatusService from '../service';
+import ConnectionStatusComponent from './component';
+
+const connectionStatusContainer = props => ;
+
+export default withTracker(() => ({
+ connectionStatus: ConnectionStatusService.getConnectionStatus(),
+}))(connectionStatusContainer);
diff --git a/src/2.7.12/imports/ui/components/connection-status/modal/styles.js b/src/2.7.12/imports/ui/components/connection-status/modal/styles.js
new file mode 100644
index 00000000..b921df3d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/modal/styles.js
@@ -0,0 +1,417 @@
+import styled from 'styled-components';
+import ModalSimple from '/imports/ui/components/common/modal/simple/component';
+import {
+ colorOffWhite,
+ colorGrayDark,
+ colorGrayLightest,
+ colorPrimary,
+ colorWhite,
+ btnPrimaryActiveBg,
+ colorDanger,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import {
+ smPaddingX,
+ smPaddingY,
+ mdPaddingY,
+ lgPaddingY,
+ titlePositionLeft,
+ mdPaddingX,
+ borderSizeLarge,
+ jumboPaddingY,
+} from '/imports/ui/stylesheets/styled-components/general';
+import {
+ fontSizeSmall,
+ fontSizeXL,
+} from '/imports/ui/stylesheets/styled-components/typography';
+import {
+ hasPhoneDimentions,
+ mediumDown,
+ hasPhoneWidth,
+ smallOnly,
+} from '/imports/ui/stylesheets/styled-components/breakpoints';
+import {
+ ScrollboxVertical,
+} from '/imports/ui/stylesheets/styled-components/scrollable';
+import {
+ Tab, Tabs, TabList, TabPanel,
+} from 'react-tabs';
+
+const Item = styled.li`
+ display: flex;
+ width: 100%;
+ height: 4rem;
+ border-bottom: 1px solid ${colorGrayLightest};
+
+ ${({ last }) => last && `
+ border: none;
+ `}
+`;
+
+const Left = styled.div`
+ display: flex;
+ width: 100%;
+ height: 100%;
+`;
+
+const Name = styled.div`
+ display: flex;
+ width: 27.5%;
+ height: 100%;
+ align-items: center;
+ justify-content: flex-start;
+
+ @media ${hasPhoneDimentions} {
+ width: 100%;
+ }
+`;
+
+const FullName = styled(Name)`
+ width: 100%;
+`;
+
+const ClientNotRespondingText = styled.div`
+ display: flex;
+ width: 27.5%;
+ height: 100%;
+ align-items: center;
+ justify-content: flex-start;
+ color: ${colorDanger};
+
+ @media ${hasPhoneDimentions} {
+ width: 100%;
+ }
+`;
+
+
+const Text = styled.div`
+ padding-left: .5rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ ${({ offline }) => offline && `
+ font-style: italic;
+ `}
+
+ [dir="rtl"] & {
+ padding: 0;
+ padding-right: .5rem;
+ }
+`;
+
+const Avatar = styled.div`
+ display: flex;
+ height: 100%;
+ justify-content: center;
+ align-items: center;
+`;
+
+const Icon = styled.div`
+ width: 2.05rem;
+ height: 2.05rem;
+`;
+
+const Right = styled.div`
+ display: flex;
+ width: 5rem;
+ height: 100%;
+`;
+
+const Time = styled.div`
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ justify-content: flex-end;
+`;
+
+const NetworkDataContainer = styled(ScrollboxVertical)`
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-wrap: nowrap;
+ overflow: auto;
+ scroll-snap-type: x mandatory;
+ padding-bottom: 1.25rem;
+
+ &:focus {
+ outline: none;
+
+ &::-webkit-scrollbar-thumb {
+ background: rgba(0,0,0,.5);
+ }
+ }
+
+ @media ${mediumDown} {
+ justify-content: space-between;
+ }
+`;
+
+const NetworkData = styled.div`
+ font-size: ${fontSizeSmall};
+
+ ${({ invisible }) => invisible && `
+ visibility: hidden;
+ `}
+
+ & :first-child {
+ font-weight: 600;
+ }
+`;
+
+const CopyContainer = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: flex-end;
+ border: none;
+ border-top: 1px solid ${colorOffWhite};
+ padding: ${jumboPaddingY} 0 0;
+`;
+
+const ConnectionStatusModal = styled(ModalSimple)`
+ padding: 1rem;
+ height: 28rem;
+
+`;
+
+const Container = styled.div`
+ padding: 0 calc(${mdPaddingX} / 2 + ${borderSizeLarge});
+`;
+
+const Header = styled.div`
+ margin: 0;
+ padding: 0;
+ border: none;
+ line-height: ${titlePositionLeft};
+ margin-bottom: ${lgPaddingY};
+`;
+
+const Title = styled.h2`
+ color: ${colorGrayDark};
+ font-weight: 500;
+ font-size: ${fontSizeXL};
+ text-align: left;
+ margin: 0;
+
+ [dir="rtl"] & {
+ text-align: right;
+ }
+`;
+
+const Content = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 0;
+`;
+
+const Wrapper = styled.div`
+ display: block;
+ width: 100%;
+ max-height: 16rem;
+`;
+
+const Status = styled.div`
+ display: flex;
+ width: 6rem;
+ height: 100%;
+ justify-content: center;
+ align-items: center;
+`;
+
+const Copy = styled.span`
+ cursor: pointer;
+ color: ${colorPrimary};
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ ${({ disabled }) => disabled && `
+ cursor: not-allowed !important;
+ `}
+`;
+
+const HelperWrapper = styled.div`
+ min-width: 12.5rem;
+ height: 100%;
+
+ @media ${mediumDown} {
+ flex: none;
+ width: 100%;
+ scroll-snap-align: start;
+ display: flex;
+ justify-content: center;
+ }
+`;
+
+const Helper = styled.div`
+ width: 12.5rem;
+ height: 100%;
+ border-radius: .5rem;
+ background-color: ${colorOffWhite};
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding: .5rem;
+`;
+
+const NetworkDataContent = styled.div`
+ margin: 0;
+ display: flex;
+ justify-content: space-around;
+ flex-grow: 1;
+
+ @media ${mediumDown} {
+ flex: none;
+ width: 100%;
+ scroll-snap-align: start;
+ }
+`;
+
+const DataColumn = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ @media ${hasPhoneWidth} {
+ flex-grow: 1;
+ }
+`;
+
+const ConnectionTabs = styled(Tabs)`
+ display: flex;
+ flex-flow: column;
+ justify-content: flex-start;
+
+ @media ${smallOnly} {
+ width: 100%;
+ flex-flow: column;
+ }
+`;
+
+const ConnectionTabList = styled(TabList)`
+ display: flex;
+ flex-flow: row;
+ margin: 0;
+ margin-bottom: .5rem;
+ border: none;
+ padding: 0;
+ width: calc(100% / 3);
+
+ @media ${smallOnly} {
+ width: 100%;
+ flex-flow: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+`;
+
+const ConnectionTabPanel = styled(TabPanel)`
+ display: none;
+ margin: 0 0 0 1rem;
+ height: 13rem;
+
+ [dir="rtl"] & {
+ margin: 0 1rem 0 0;
+ }
+
+ &.is-selected {
+ display: flex;
+ flex-flow: column;
+ }
+
+ & ul {
+ padding: 0;
+ }
+
+ @media ${smallOnly} {
+ width: 100%;
+ margin: 0;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+`;
+
+const ConnectionTabSelector = styled(Tab)`
+ display: flex;
+ flex-flow: row;
+ font-size: 0.9rem;
+ flex: 0 0 auto;
+ justify-content: flex-start;
+ border: none !important;
+ padding: ${mdPaddingY} ${mdPaddingX};
+
+ border-radius: .2rem;
+ cursor: pointer;
+ margin-bottom: ${smPaddingY};
+ align-items: center;
+ flex-grow: 0;
+ min-width: 0;
+
+ & > span {
+ min-width: 0;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ @media ${smallOnly} {
+ max-width: 100%;
+ margin: 0 ${smPaddingX} 0 0;
+ & > i {
+ display: none;
+ }
+
+ [dir="rtl"] & {
+ margin: 0 0 0 ${smPaddingX};
+ }
+ }
+
+ span {
+ border-bottom: 2px solid ${colorWhite};
+ }
+
+ &.is-selected {
+ border: none;
+ color: ${colorPrimary};
+
+ span {
+ border-bottom: 2px solid ${btnPrimaryActiveBg};
+ }
+ }
+`;
+
+export default {
+ Item,
+ Left,
+ Name,
+ Text,
+ Avatar,
+ Icon,
+ Right,
+ Time,
+ NetworkDataContainer,
+ NetworkData,
+ CopyContainer,
+ ConnectionStatusModal,
+ ClientNotRespondingText,
+ Container,
+ Header,
+ Title,
+ Content,
+ Wrapper,
+ Status,
+ Copy,
+ Helper,
+ NetworkDataContent,
+ FullName,
+ DataColumn,
+ HelperWrapper,
+ ConnectionTabs,
+ ConnectionTabList,
+ ConnectionTabSelector,
+ ConnectionTabPanel,
+};
diff --git a/src/2.7.12/imports/ui/components/connection-status/service.js b/src/2.7.12/imports/ui/components/connection-status/service.js
new file mode 100644
index 00000000..bd6ffa4d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/service.js
@@ -0,0 +1,561 @@
+import { defineMessages } from 'react-intl';
+import ConnectionStatus from '/imports/api/connection-status';
+import Users from '/imports/api/users';
+import UsersPersistentData from '/imports/api/users-persistent-data';
+import Auth from '/imports/ui/services/auth';
+import { Session } from 'meteor/session';
+import { notify } from '/imports/ui/services/notification';
+import { makeCall } from '/imports/ui/services/api';
+import AudioService from '/imports/ui/components/audio/service';
+import VideoService from '/imports/ui/components/video-provider/service';
+import ScreenshareService from '/imports/ui/components/screenshare/service';
+
+const STATS = Meteor.settings.public.stats;
+const NOTIFICATION = STATS.notification;
+const STATS_INTERVAL = STATS.interval;
+const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
+
+const intlMessages = defineMessages({
+ saved: {
+ id: 'app.settings.save-notification.label',
+ description: 'Label shown in toast when data savings are saved',
+ },
+ notification: {
+ id: 'app.connection-status.notification',
+ description: 'Label shown in toast when connection loss is detected',
+ },
+});
+
+let lastLevel = -1;
+let lastRtt = null;
+const levelDep = new Tracker.Dependency();
+
+let statsTimeout = null;
+
+const URL_REGEX = new RegExp(/^(http|https):\/\/[^ "]+$/);
+const getHelp = () => {
+ if (URL_REGEX.test(STATS.help)) return STATS.help;
+
+ return null;
+};
+
+const getStats = () => {
+ levelDep.depend();
+ return STATS.level[lastLevel];
+};
+
+const setStats = (level = -1, type = 'recovery', value = {}) => {
+ if (lastLevel !== level) {
+ lastLevel = level;
+ levelDep.changed();
+ }
+ addConnectionStatus(level, type, value);
+};
+
+const handleAudioStatsEvent = (event) => {
+ const { detail } = event;
+ if (detail) {
+ const { loss, jitter } = detail;
+ let active = false;
+ // From higher to lower
+ for (let i = STATS.level.length - 1; i >= 0; i--) {
+ if (loss >= STATS.loss[i] || jitter >= STATS.jitter[i]) {
+ active = true;
+ setStats(i, 'audio', { loss, jitter });
+ break;
+ }
+ }
+
+ if (active) startStatsTimeout();
+ }
+};
+
+const handleSocketStatsEvent = (event) => {
+ const { detail } = event;
+ if (detail) {
+ const { rtt } = detail;
+ let active = false;
+ let level = -1;
+ // From higher to lower
+ for (let i = STATS.level.length - 1; i >= 0; i--) {
+ if (rtt >= STATS.rtt[i]) {
+ active = true;
+ level = i;
+ break;
+ }
+ }
+
+ setStats(level, 'socket', { rtt });
+
+ if (active) startStatsTimeout();
+ }
+};
+
+const startStatsTimeout = () => {
+ if (statsTimeout !== null) clearTimeout(statsTimeout);
+
+ statsTimeout = setTimeout(() => {
+ setStats(-1, 'recovery', {});
+ }, STATS.timeout);
+};
+
+const addConnectionStatus = (level, type, value) => {
+ const status = level !== -1 ? STATS.level[level] : 'normal';
+
+ makeCall('addConnectionStatus', status, type, value);
+};
+
+let rttCalcStartedAt = 0;
+
+const fetchRoundTripTime = () => {
+ // if client didn't receive response from last "voidConnection"
+ // calculate the rtt from last call time and notify user of connection loss
+ if (rttCalcStartedAt !== 0) {
+ const tf = Date.now();
+ const rtt = tf - rttCalcStartedAt;
+
+ if (rtt > STATS.rtt[STATS.rtt.length - 1]) {
+ const event = new CustomEvent('socketstats', { detail: { rtt } });
+ window.dispatchEvent(event);
+ }
+ }
+
+ const t0 = Date.now();
+ rttCalcStartedAt = t0;
+
+ makeCall('voidConnection', lastRtt).then(() => {
+ const tf = Date.now();
+ const rtt = tf - t0;
+ const event = new CustomEvent('socketstats', { detail: { rtt } });
+ window.dispatchEvent(event);
+ lastRtt = rtt;
+
+ rttCalcStartedAt = 0;
+ });
+};
+
+const sortLevel = (a, b) => {
+ const indexOfA = STATS.level.indexOf(a.level);
+ const indexOfB = STATS.level.indexOf(b.level);
+
+ if (indexOfA < indexOfB) return 1;
+ if (indexOfA === indexOfB) return 0;
+ if (indexOfA > indexOfB) return -1;
+};
+
+const sortOffline = (a, b) => {
+ if (a.offline && !b.offline) return 1;
+ if (a.offline === b.offline) return 0;
+ if (!a.offline && b.offline) return -1;
+};
+
+const getConnectionStatus = () => {
+ const selector = {
+ meetingId: Auth.meetingID,
+ $or: [
+ { status: { $exists: true } },
+ { clientNotResponding: true },
+ ],
+ };
+
+ if (!isModerator()) {
+ selector.userId = Auth.userID;
+ }
+
+ const connectionStatus = ConnectionStatus.find(selector).fetch().map((userStatus) => {
+ const {
+ userId,
+ status,
+ statusUpdatedAt,
+ clientNotResponding,
+ } = userStatus;
+
+ return {
+ userId,
+ status,
+ statusUpdatedAt,
+ clientNotResponding,
+ };
+ });
+
+ return UsersPersistentData.find(
+ { meetingId: Auth.meetingID },
+ {
+ fields:
+ {
+ userId: 1,
+ name: 1,
+ role: 1,
+ avatar: 1,
+ color: 1,
+ loggedOut: 1,
+ },
+ },
+ ).fetch().reduce((result, user) => {
+ const {
+ userId,
+ name,
+ role,
+ avatar,
+ color,
+ loggedOut,
+ } = user;
+
+ const userStatus = connectionStatus.find((userConnStatus) => userConnStatus.userId === userId);
+
+ if (userStatus) {
+ if (userStatus.status || (!loggedOut && userStatus.clientNotResponding)) {
+ result.push({
+ userId,
+ name,
+ avatar,
+ offline: loggedOut,
+ notResponding: userStatus.clientNotResponding,
+ you: Auth.userID === userId,
+ moderator: role === ROLE_MODERATOR,
+ color,
+ status: userStatus.clientNotResponding ? 'critical' : userStatus.status,
+ timestamp: userStatus.statusUpdatedAt,
+ });
+ }
+ }
+
+ return result;
+ }, []).sort(sortLevel).sort(sortOffline);
+};
+
+const isEnabled = () => STATS.enabled;
+
+let roundTripTimeInterval = null;
+
+const startRoundTripTime = () => {
+ if (!isEnabled()) return;
+
+ stopRoundTripTime();
+
+ roundTripTimeInterval = setInterval(fetchRoundTripTime, STATS_INTERVAL);
+};
+
+const stopRoundTripTime = () => {
+ if (roundTripTimeInterval) {
+ clearInterval(roundTripTimeInterval);
+ }
+};
+
+const isModerator = () => {
+ const user = Users.findOne(
+ {
+ meetingId: Auth.meetingID,
+ userId: Auth.userID,
+ },
+ { fields: { role: 1 } },
+ );
+
+ if (user && user.role === ROLE_MODERATOR) {
+ return true;
+ }
+
+ return false;
+};
+
+if (STATS.enabled) {
+ window.addEventListener('audiostats', handleAudioStatsEvent);
+ window.addEventListener('socketstats', handleSocketStatsEvent);
+}
+
+const getNotified = () => {
+ const notified = Session.get('connectionStatusNotified');
+
+ // Since notified can be undefined we need a boolean verification
+ return notified === true;
+};
+
+const notification = (level, intl) => {
+ if (!NOTIFICATION[level]) return null;
+
+ // Avoid toast spamming
+ const notified = getNotified();
+ if (notified) {
+ return null;
+ }
+ Session.set('connectionStatusNotified', true);
+
+
+ if (intl) notify(intl.formatMessage(intlMessages.notification), level, 'warning');
+};
+
+/**
+ * Calculates the jitter buffer average.
+ * For more information see:
+ * https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-jitterbufferdelay
+ * @param {Object} inboundRtpData The RTCInboundRtpStreamStats object retrieved
+ * in getStats() call.
+ * @returns The jitter buffer average in ms
+ */
+const calculateJitterBufferAverage = (inboundRtpData) => {
+ if (!inboundRtpData) return 0;
+
+ const {
+ jitterBufferDelay,
+ jitterBufferEmittedCount,
+ } = inboundRtpData;
+
+ if (!jitterBufferDelay || !jitterBufferEmittedCount) return '--';
+
+ return Math.round((jitterBufferDelay / jitterBufferEmittedCount) * 1000);
+};
+
+/**
+ * Given the data returned from getStats(), returns an array containing all the
+ * the stats of the given type.
+ * For more information see:
+ * https://developer.mozilla.org/en-US/docs/Web/API/RTCStatsReport
+ * and
+ * https://developer.mozilla.org/en-US/docs/Web/API/RTCStatsType
+ * @param {Object} data - RTCStatsReport object returned from getStats() API
+ * @param {String} type - The string type corresponding to RTCStatsType object
+ * @returns {Array[Object]} An array containing all occurrences of the given
+ * type in the data Object.
+ */
+const getDataType = (data, type) => {
+ if (!data || typeof data !== 'object' || !type) return [];
+
+ return Object.values(data).filter((stat) => stat.type === type);
+};
+
+/**
+ * Returns a new Object containing extra parameters calculated from inbound
+ * data. The input data is also appended in the returned Object.
+ * @param {Object} currentData - The object returned from getStats / service's
+ * getNetworkData()
+ * @returns {Object} the currentData object with the extra inbound network
+ * added to it.
+ */
+const addExtraInboundNetworkParameters = (data) => {
+ if (!data) return data;
+
+ const inboundRtpData = getDataType(data, 'inbound-rtp')[0];
+
+ if (!inboundRtpData) return data;
+
+ const extraParameters = {
+ jitterBufferAverage: calculateJitterBufferAverage(inboundRtpData),
+ packetsLost: inboundRtpData.packetsLost,
+ };
+
+ return Object.assign(inboundRtpData, extraParameters);
+};
+
+/**
+ * Retrieves the inbound and outbound data using WebRTC getStats API, for audio.
+ * @returns An Object with format (property:type) :
+ * {
+ * transportStats: Object,
+ * inbound-rtp: RTCInboundRtpStreamStats,
+ * outbound-rtp: RTCOutboundRtpStreamStats,
+ * }
+ * For more information see:
+ * https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats
+ * and
+ * https://www.w3.org/TR/webrtc-stats/#dom-rtcoutboundrtpstreamstats
+ */
+const getAudioData = async () => {
+ const data = await AudioService.getStats();
+
+ if (!data) return {};
+
+ addExtraInboundNetworkParameters(data);
+
+ return data;
+};
+
+/**
+ * Retrieves the inbound and outbound data using WebRTC getStats API, for video.
+ * The video stats contains the stats about all video peers (cameras) and
+ * for screenshare peer appended into one single object, containing the id
+ * of the peers with it's stats information.
+ * @returns An Object containing video data for all video peers and screenshare
+ * peer
+ */
+const getVideoData = async () => {
+ const camerasData = await VideoService.getStats() || {};
+
+ const screenshareData = await ScreenshareService.getStats() || {};
+
+ return {
+ ...camerasData,
+ ...screenshareData,
+ };
+};
+
+/**
+ * Get the user, audio and video data from current active streams.
+ * For audio, this will get information about the mic/listen-only stream.
+ * @returns An Object containing all this data.
+ */
+const getNetworkData = async () => {
+ const audio = await getAudioData();
+
+ const video = await getVideoData();
+
+ const user = {
+ time: new Date(),
+ username: Auth.username,
+ meeting_name: Auth.confname,
+ meeting_id: Auth.meetingID,
+ connection_id: Auth.connectionID,
+ user_id: Auth.userID,
+ extern_user_id: Auth.externUserID,
+ };
+
+ const fullData = {
+ user,
+ audio,
+ video,
+ };
+
+ return fullData;
+};
+
+/**
+ * Calculates both upload and download rates using data retreived from getStats
+ * API. For upload (outbound-rtp) we use both bytesSent and timestamp fields.
+ * byteSent field contains the number of octets sent at the given timestamp,
+ * more information can be found in:
+ * https://www.w3.org/TR/webrtc-stats/#dom-rtcsentrtpstreamstats-bytessent
+ *
+ * timestamp is given in millisseconds, more information can be found in:
+ * https://www.w3.org/TR/webrtc-stats/#webidl-1049090475
+ * @param {Object} currentData - The object returned from getStats / service's
+ * getNetworkData()
+ * @param {Object} previousData - The same object as above, but representing
+ * a data collected in past (previous call of
+ * service's getNetworkData())
+ * @returns An object of numbers, containing both outbound (upload) and inbound
+ * (download) rates (kbps).
+ */
+const calculateBitsPerSecond = (currentData, previousData) => {
+ const result = {
+ outbound: 0,
+ inbound: 0,
+ };
+
+ if (!currentData || !previousData) return result;
+
+ const currentOutboundData = getDataType(currentData, 'outbound-rtp')[0];
+ const currentInboundData = getDataType(currentData, 'inbound-rtp')[0];
+ const previousOutboundData = getDataType(previousData, 'outbound-rtp')[0];
+ const previousInboundData = getDataType(previousData, 'inbound-rtp')[0];
+
+ if (currentOutboundData && previousOutboundData) {
+ const {
+ bytesSent: outboundBytesSent,
+ timestamp: outboundTimestamp,
+ } = currentOutboundData;
+
+ let {
+ headerBytesSent: outboundHeaderBytesSent,
+ } = currentOutboundData;
+
+ if (!outboundHeaderBytesSent) outboundHeaderBytesSent = 0;
+
+ const {
+ bytesSent: previousOutboundBytesSent,
+ timestamp: previousOutboundTimestamp,
+ } = previousOutboundData;
+
+ let {
+ headerBytesSent: previousOutboundHeaderBytesSent,
+ } = previousOutboundData;
+
+ if (!previousOutboundHeaderBytesSent) previousOutboundHeaderBytesSent = 0;
+
+ const outboundBytesPerSecond = (outboundBytesSent + outboundHeaderBytesSent
+ - previousOutboundBytesSent - previousOutboundHeaderBytesSent)
+ / (outboundTimestamp - previousOutboundTimestamp);
+
+ result.outbound = Math.round((outboundBytesPerSecond * 8 * 1000) / 1024);
+ }
+
+ if (currentInboundData && previousInboundData) {
+ const {
+ bytesReceived: inboundBytesReceived,
+ timestamp: inboundTimestamp,
+ } = currentInboundData;
+
+ let {
+ headerBytesReceived: inboundHeaderBytesReceived,
+ } = currentInboundData;
+
+ if (!inboundHeaderBytesReceived) inboundHeaderBytesReceived = 0;
+
+ const {
+ bytesReceived: previousInboundBytesReceived,
+ timestamp: previousInboundTimestamp,
+ } = previousInboundData;
+
+ let {
+ headerBytesReceived: previousInboundHeaderBytesReceived,
+ } = previousInboundData;
+
+ if (!previousInboundHeaderBytesReceived) {
+ previousInboundHeaderBytesReceived = 0;
+ }
+
+ const inboundBytesPerSecond = (inboundBytesReceived
+ + inboundHeaderBytesReceived - previousInboundBytesReceived
+ - previousInboundHeaderBytesReceived) / (inboundTimestamp
+ - previousInboundTimestamp);
+
+ result.inbound = Math.round((inboundBytesPerSecond * 8 * 1000) / 1024);
+ }
+
+ return result;
+};
+
+/**
+ * Similar to calculateBitsPerSecond, but it receives stats from multiple
+ * peers. The total inbound/outbound is the sum of all peers.
+ * @param {Object} currentData - The Object returned from
+ * getStats / service's getNetworkData()
+ * @param {Object} previousData - The same object as above, but
+ * representing a data collected in past
+ * (previous call of service's getNetworkData())
+ */
+const calculateBitsPerSecondFromMultipleData = (currentData, previousData) => {
+ const result = {
+ outbound: 0,
+ inbound: 0,
+ };
+
+ if (!currentData || !previousData) return result;
+
+ Object.keys(currentData).forEach((peerId) => {
+ if (previousData[peerId]) {
+ const {
+ outbound: peerOutbound,
+ inbound: peerInbound,
+ } = calculateBitsPerSecond(currentData[peerId], previousData[peerId]);
+
+ result.outbound += peerOutbound;
+ result.inbound += peerInbound;
+ }
+ });
+
+ return result;
+};
+
+export default {
+ isModerator,
+ getConnectionStatus,
+ getStats,
+ getHelp,
+ isEnabled,
+ notification,
+ startRoundTripTime,
+ stopRoundTripTime,
+ getNetworkData,
+ calculateBitsPerSecond,
+ calculateBitsPerSecondFromMultipleData,
+ getDataType,
+};
diff --git a/src/2.7.12/imports/ui/components/connection-status/status-helper/component.jsx b/src/2.7.12/imports/ui/components/connection-status/status-helper/component.jsx
new file mode 100644
index 00000000..45cd711e
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/status-helper/component.jsx
@@ -0,0 +1,113 @@
+import React, { Fragment, PureComponent } from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import Styled from './styles';
+import Icon from '/imports/ui/components/connection-status/icon/component';
+import SettingsMenuContainer from '/imports/ui/components/settings/container';
+
+const intlMessages = defineMessages({
+ label: {
+ id: 'app.connection-status.label',
+ description: 'Connection status label',
+ },
+ settings: {
+ id: 'app.connection-status.settings',
+ description: 'Connection settings label',
+ },
+});
+
+class ConnectionStatusIcon extends PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.state = { isSettingsMenuModalOpen: false };
+
+ this.setSettingsMenuModalIsOpen = this.setSettingsMenuModalIsOpen.bind(this);
+ }
+
+ renderIcon(level = 'normal') {
+ return(
+
+
+
+ );
+ }
+
+ openAdjustSettings() {
+ this.setSettingsMenuModalIsOpen(true);
+ }
+
+ setSettingsMenuModalIsOpen(value) {
+ const {closeModal} = this.props;
+
+ this.setState({isSettingsMenuModalOpen: value})
+ if (!value) {
+ closeModal();
+ }
+ }
+
+ render() {
+ const {
+ intl,
+ stats,
+ } = this.props;
+
+ let color;
+ switch (stats) {
+ case 'warning':
+ color = 'success';
+ break;
+ case 'danger':
+ color = 'warning';
+ break;
+ case 'critical':
+ color = 'danger';
+ break;
+ default:
+ color = 'success';
+ }
+
+ const level = stats ? stats : 'normal';
+
+ const { isSettingsMenuModalOpen } = this.state;
+
+ return (
+
+
+ {this.renderIcon(level)}
+
+
+ {intl.formatMessage(intlMessages.label)}
+
+ {(level === 'critical' || level === 'danger') || isSettingsMenuModalOpen
+ ? (
+
+
+ {intl.formatMessage(intlMessages.settings)}
+
+ {isSettingsMenuModalOpen ? this.setSettingsMenuModalIsOpen(false),
+ priority: "medium",
+ setIsOpen: this.setSettingsMenuModalIsOpen,
+ isOpen: isSettingsMenuModalOpen,
+ }}
+ /> : null}
+
+ )
+ : (
+
+ )
+ }
+
+ );
+ }
+}
+
+export default injectIntl(ConnectionStatusIcon);
diff --git a/src/2.7.12/imports/ui/components/connection-status/status-helper/container.jsx b/src/2.7.12/imports/ui/components/connection-status/status-helper/container.jsx
new file mode 100644
index 00000000..c35110cb
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/status-helper/container.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import ConnectionStatusService from '../service';
+import ConnectionStatusIconComponent from './component';
+
+const connectionStatusIconContainer = props => ;
+
+export default withTracker(() => {
+ return {
+ stats: ConnectionStatusService.getStats(),
+ };
+})(connectionStatusIconContainer);
diff --git a/src/2.7.12/imports/ui/components/connection-status/status-helper/styles.js b/src/2.7.12/imports/ui/components/connection-status/status-helper/styles.js
new file mode 100644
index 00000000..cca6cb4f
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/connection-status/status-helper/styles.js
@@ -0,0 +1,45 @@
+import styled from 'styled-components';
+import {
+ colorPrimary,
+ colorSuccess,
+ colorWarning,
+ colorDanger,
+} from '/imports/ui/stylesheets/styled-components/palette';
+
+const StatusIconWrapper = styled.div`
+ border-radius: 50%;
+ padding: 1.5rem;
+
+ ${({ color }) => {
+ let bgColor = colorSuccess;
+ bgColor = color === 'warning' ? colorWarning : bgColor;
+ bgColor = color === 'danger' ? colorDanger : bgColor;
+ return `background-color: ${bgColor};`
+ }}
+`;
+
+const IconWrapper = styled.div`
+ width: 2.25rem;
+ height: 2.25rem;
+`;
+
+const Label = styled.div`
+ font-weight: 600;
+ margin: .25rem 0 .5rem;
+`;
+
+const Settings = styled.span`
+ color: ${colorPrimary};
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+`;
+
+export default {
+ StatusIconWrapper,
+ IconWrapper,
+ Label,
+ Settings,
+};
diff --git a/src/2.7.12/imports/ui/components/context-providers/component.jsx b/src/2.7.12/imports/ui/components/context-providers/component.jsx
new file mode 100644
index 00000000..a5b869b0
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/context-providers/component.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { ChatContextProvider } from '/imports/ui/components/components-data/chat-context/context';
+import { UsersContextProvider } from '/imports/ui/components/components-data/users-context/context';
+import { GroupChatContextProvider } from '/imports/ui/components/components-data/group-chat-context/context';
+import { LayoutContextProvider } from '/imports/ui/components/layout/context';
+import { CustomBackgroundsProvider } from '/imports/ui/components/video-preview/virtual-background/context';
+
+const providersList = [
+ ChatContextProvider,
+ GroupChatContextProvider,
+ UsersContextProvider,
+ LayoutContextProvider,
+ CustomBackgroundsProvider,
+];
+
+const ContextProvidersComponent = props => providersList.reduce((acc, Component) => (
+
+ {acc}
+ ), props.children);
+
+export default ContextProvidersComponent;
diff --git a/src/2.7.12/imports/ui/components/debug-window/component.jsx b/src/2.7.12/imports/ui/components/debug-window/component.jsx
new file mode 100644
index 00000000..6243a51b
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/debug-window/component.jsx
@@ -0,0 +1,203 @@
+import React, { Component } from 'react';
+import Draggable from 'react-draggable';
+import { defineMessages, injectIntl } from 'react-intl';
+import Styled from './styles';
+import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
+
+const intlMessages = defineMessages({
+ modalClose: {
+ id: 'app.modal.close',
+ description: 'Close',
+ },
+ modalCloseDescription: {
+ id: 'app.modal.close.description',
+ description: 'Disregards changes and closes the modal',
+ },
+ debugWindowTitle: {
+ id: 'app.debugWindow.windowTitle',
+ description: 'Debug window title',
+ },
+ userAgentLabel: {
+ id: 'app.debugWindow.form.userAgentLabel',
+ description: 'User agent form label',
+ },
+ copyButtonLabel: {
+ id: 'app.debugWindow.form.button.copy',
+ description: 'User agent form copy button',
+ },
+ chatLoggerLabel: {
+ id: 'app.debugWindow.form.chatLoggerLabel',
+ description: 'Chat logger level form label',
+ },
+ applyButtonLabel: {
+ id: 'app.debugWindow.form.button.apply',
+ description: 'Chat logger level form apply button',
+ },
+ on: {
+ id: 'app.switch.onLabel',
+ description: 'label for toggle switch on state',
+ },
+ off: {
+ id: 'app.switch.offLabel',
+ description: 'label for toggle switch off state',
+ },
+});
+
+const DEBUG_WINDOW_ENABLED = Meteor.settings.public.app.enableDebugWindow;
+const SHOW_DEBUG_WINDOW_ACCESSKEY = Meteor.settings.public.app.shortcuts.openDebugWindow.accesskey;
+
+class DebugWindow extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ showDebugWindow: false,
+ logLevel: ChatLogger.getLogLevel(),
+ };
+ }
+
+ componentDidMount() {
+ document.addEventListener('keyup', (event) => {
+ const { key, code } = event;
+ const eventKey = key?.toUpperCase();
+ const eventCode = code;
+ if (DEBUG_WINDOW_ENABLED && event?.altKey && (eventKey === SHOW_DEBUG_WINDOW_ACCESSKEY || eventCode === `Key${SHOW_DEBUG_WINDOW_ACCESSKEY}`)) {
+ this.debugWindowToggle();
+ }
+ });
+ }
+
+ setShowDebugWindow(showDebugWindow) {
+ this.setState({ showDebugWindow });
+ }
+
+ debugWindowToggle() {
+ const { showDebugWindow } = this.state;
+ if (showDebugWindow) {
+ this.setShowDebugWindow(false);
+ } else {
+ this.setShowDebugWindow(true);
+ }
+ }
+
+ displaySettingsStatus(status) {
+ const { intl } = this.props;
+
+ return (
+
+ {status ? intl.formatMessage(intlMessages.on)
+ : intl.formatMessage(intlMessages.off)}
+
+ );
+ }
+
+ render() {
+ const { showDebugWindow, logLevel } = this.state;
+ const chatLoggerLevelsNames = Object.keys(ChatLogger.levels);
+
+ if (!DEBUG_WINDOW_ENABLED || !showDebugWindow) return false;
+
+ const { intl } = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+ {`${intl.formatMessage(intlMessages.userAgentLabel)}:`}
+
+
+
+ navigator.clipboard.writeText(window.navigator.userAgent)}
+ >
+ {`${intl.formatMessage(intlMessages.copyButtonLabel)}`}
+
+
+
+
+
+ {`${intl.formatMessage(intlMessages.chatLoggerLabel)}:`}
+
+
+
+ {
+ this.setState({
+ logLevel: ev.target.value,
+ });
+ }}
+ >
+ {
+ chatLoggerLevelsNames.map((i, index) => {
+ const idx = index;
+ return ({i} );
+ })
+ }
+
+ {
+ ChatLogger.setLogLevel(logLevel);
+ this.setState({
+ logLevel: ChatLogger.getLogLevel(),
+ });
+ }}
+ >
+ {`${intl.formatMessage(intlMessages.applyButtonLabel)}`}
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default injectIntl(DebugWindow);
diff --git a/src/2.7.12/imports/ui/components/debug-window/styles.js b/src/2.7.12/imports/ui/components/debug-window/styles.js
new file mode 100644
index 00000000..a1be2e7c
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/debug-window/styles.js
@@ -0,0 +1,132 @@
+import styled from 'styled-components';
+import { smPaddingX } from '/imports/ui/stylesheets/styled-components/general';
+import Resizable from 're-resizable';
+import Icon from '/imports/ui/components/common/icon/component';
+import Button from '/imports/ui/components/common/button/component';
+
+const ToggleLabel = styled.span`
+ margin-right: ${smPaddingX};
+
+ [dir="rtl"] & {
+ margin: 0 0 0 ${smPaddingX};
+ }
+`;
+
+const DebugWindowWrapper = styled(Resizable)`
+ position: absolute !important;
+ z-index: 9;
+`;
+
+const DebugWindow = styled.div`
+ min-width: 20vw;
+ min-height: 20vh;
+ width: 100%;
+ height: 100%;
+ background-color: white;
+ border: 2px solid #06172A;
+
+ &::after {
+ content: "";
+ -webkit-transform: rotate(-45deg);
+ position: absolute;
+ right: 2px;
+ bottom: 8px;
+ pointer-events: none;
+ width: 14px;
+ height: 1px;
+ background: rgba(0,0,0,.5);
+ }
+
+ &::before {
+ content: "";
+ -webkit-transform: rotate(-45deg);
+ position: absolute;
+ right: 2px;
+ bottom: 5px;
+ pointer-events: none;
+ width: 8px;
+ height: 1px;
+ background: rgba(0,0,0,.5);
+ }
+`;
+
+const Header = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid lightgray;
+ cursor: move;
+
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+`;
+
+const MoveIcon = styled(Icon)`
+ margin: 5px;
+ color: rgba(0,0,0,.5);
+`;
+
+const Title = styled.div`
+ font-size: 1.2rem;
+ font-weight: bold;
+`;
+
+const CloseButton = styled(Button)`
+ & > span > i {
+ font-size: 115%;
+ }
+`;
+
+const DebugWindowContent = styled.div`
+ padding: 10px;
+`;
+
+const Table = styled.div`
+ display: table;
+ width: 100%;
+`;
+
+const TableRow = styled.div`
+ display: table-row;
+`;
+
+const TableCell = styled.div`
+ display: table-cell;
+ padding: 5px;
+ vertical-align: middle;
+`;
+
+const UserAgentInput = styled.input`
+ margin-right: 5px;
+`;
+
+const AutoArrangeToggle = styled.input`
+ margin-right: 5px;
+`;
+
+const CellContent = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+export default {
+ ToggleLabel,
+ DebugWindowWrapper,
+ DebugWindow,
+ Header,
+ MoveIcon,
+ Title,
+ CloseButton,
+ DebugWindowContent,
+ Table,
+ TableRow,
+ TableCell,
+ UserAgentInput,
+ AutoArrangeToggle,
+ CellContent,
+};
diff --git a/src/2.7.12/imports/ui/components/dropdown/component.jsx b/src/2.7.12/imports/ui/components/dropdown/component.jsx
new file mode 100644
index 00000000..3c4ffa93
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/component.jsx
@@ -0,0 +1,330 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { findDOMNode } from 'react-dom';
+import TetherComponent from 'react-tether';
+import { defineMessages, injectIntl } from 'react-intl';
+import deviceInfo from '/imports/utils/deviceInfo';
+import screenreaderTrap from 'makeup-screenreader-trap';
+import { Session } from 'meteor/session';
+import Styled from './styles';
+
+import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
+import DropdownContent from '/imports/ui/components/dropdown/content/component';
+import DropdownList from '/imports/ui/components/dropdown/list/component';
+import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
+import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
+import DropdownListTitle from '/imports/ui/components/dropdown/list/title/component';
+
+const intlMessages = defineMessages({
+ close: {
+ id: 'app.dropdown.close',
+ description: 'Close button label',
+ },
+});
+
+const noop = () => { };
+
+const propTypes = {
+ /**
+ * The dropdown needs a trigger and a content component as children
+ */
+ children: (props, propName, componentName) => {
+ const children = props[propName];
+
+ if (!children || children.length < 2) {
+ return new Error(`Invalid prop \`${propName}\` supplied to`
+ + ` \`${componentName}\`. Validation failed.`);
+ }
+
+ const trigger = children.find((x) => x.type === DropdownTrigger);
+ const content = children.find((x) => x.type === DropdownContent);
+
+ if (!trigger) {
+ return new Error(`Invalid prop \`${propName}\` supplied to`
+ + ` \`${componentName}\`. Missing \`DropdownTrigger\`. Validation failed.`);
+ }
+
+ if (!content) {
+ return new Error(`Invalid prop \`${propName}\` supplied to`
+ + ` \`${componentName}\`. Missing \`DropdownContent\`. Validation failed.`);
+ }
+
+ return null;
+ },
+ isOpen: PropTypes.bool,
+ keepOpen: PropTypes.bool,
+ onHide: PropTypes.func,
+ onShow: PropTypes.func,
+ autoFocus: PropTypes.bool,
+ tethered: PropTypes.bool,
+ getContent: PropTypes.func,
+};
+
+const defaultProps = {
+ tethered: false,
+ children: null,
+ onShow: noop,
+ onHide: noop,
+ autoFocus: false,
+ isOpen: false,
+ keepOpen: null,
+ getContent: () => {},
+};
+
+const attachments = {
+ 'right-bottom': 'bottom left',
+ 'right-top': 'bottom left',
+};
+const targetAttachments = {
+ 'right-bottom': 'bottom right',
+ 'right-top': 'top right',
+};
+
+class Dropdown extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpen: false,
+ isPortrait: deviceInfo.isPortrait(),
+ };
+ this.handleShow = this.handleShow.bind(this);
+ this.handleHide = this.handleHide.bind(this);
+ this.handleToggle = this.handleToggle.bind(this);
+ this.handleWindowClick = this.handleWindowClick.bind(this);
+ this.updateOrientation = this.updateOrientation.bind(this);
+ }
+
+ componentDidMount() {
+ window.addEventListener('resize', this.updateOrientation);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ const {
+ onShow,
+ onHide,
+ keepOpen,
+ tethered,
+ } = this.props;
+
+ const { isOpen } = this.state;
+
+ const enableSRTrap = isOpen && !tethered;
+
+ if (enableSRTrap) {
+ screenreaderTrap.trap(this.dropdown);
+ } else {
+ screenreaderTrap.untrap();
+ }
+
+ if (isOpen && !prevState.isOpen) { onShow(); }
+
+ if (!isOpen && prevState.isOpen) { onHide(); }
+
+ if (prevProps.keepOpen && !keepOpen) onHide();
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.updateOrientation);
+ }
+
+ handleHide() {
+ Session.set('dropdownOpen', false);
+ const { onHide } = this.props;
+ this.setState({ isOpen: false }, () => {
+ const { removeEventListener } = window;
+ onHide();
+ removeEventListener('click', this.handleWindowClick, true);
+ });
+ }
+
+ handleShow() {
+ Session.set('dropdownOpen', true);
+ const {
+ onShow,
+ } = this.props;
+ this.setState({ isOpen: true }, () => {
+ const { addEventListener } = window;
+ onShow();
+ addEventListener('click', this.handleWindowClick, true);
+ });
+ }
+
+ handleWindowClick(event) {
+ const { keepOpen, onHide } = this.props;
+ const { isOpen } = this.state;
+ const triggerElement = findDOMNode(this.trigger);
+ const contentElement = findDOMNode(this.content);
+ if (!(triggerElement && contentElement)) return;
+ if (triggerElement && triggerElement.contains(event.target)) {
+ if (keepOpen) {
+ onHide();
+ return;
+ }
+ if (isOpen) {
+ this.handleHide();
+ return;
+ }
+ }
+
+ if (keepOpen && isOpen && !contentElement.contains(event.target)) {
+ if (triggerElement) {
+ const { parentElement } = triggerElement;
+ if (parentElement) parentElement.focus();
+ }
+ onHide();
+ this.handleHide();
+ return;
+ }
+
+ if (keepOpen && triggerElement) {
+ const { parentElement } = triggerElement;
+ if (parentElement) parentElement.focus();
+ }
+
+ if (keepOpen === true) return;
+ this.handleHide();
+ }
+
+ handleToggle() {
+ const { isOpen } = this.state;
+ return isOpen ? this.handleHide() : this.handleShow();
+ }
+
+ updateOrientation() {
+ this.setState({ isPortrait: deviceInfo.isPortrait() });
+ }
+
+ render() {
+ const {
+ children,
+ intl,
+ keepOpen,
+ tethered,
+ placement,
+ getContent,
+ ...otherProps
+ } = this.props;
+ const { isOpen, isPortrait } = this.state;
+ const { isPhone } = deviceInfo;
+ const placements = placement && placement.replace(' ', '-');
+ const test = isPhone && isPortrait ? {
+ width: '100%',
+ height: '100%',
+ transform: 'translateY(0)',
+ } : {
+ width: '',
+ height: '',
+ transform: '',
+ };
+
+ let trigger = children.find((x) => x.type === DropdownTrigger);
+ let content = children.find((x) => x.type === DropdownContent);
+
+ trigger = React.cloneElement(trigger, {
+ ref: (ref) => { this.trigger = ref; },
+ dropdownIsOpen: isOpen,
+ dropdownToggle: this.handleToggle,
+ dropdownShow: this.handleShow,
+ dropdownHide: this.handleHide,
+ keepopen: `${keepOpen}`,
+ });
+
+ content = React.cloneElement(content, {
+ ref: (ref) => {
+ getContent(ref);
+ this.content = ref;
+ },
+ 'aria-expanded': isOpen,
+ dropdownIsOpen: isOpen,
+ dropdownToggle: this.handleToggle,
+ dropdownShow: this.handleShow,
+ dropdownHide: this.handleHide,
+ keepopen: `${keepOpen}`,
+ });
+
+ const showCloseBtn = (isOpen && keepOpen) || (isOpen && keepOpen === null);
+ return (
+ { this.dropdown = node; }}
+ tabIndex={-1}
+ >
+ {
+ tethered
+ ? (
+ (
+
+ {trigger}
+
+ )}
+ renderElement={(ref) => (
+
+ {content}
+ {showCloseBtn
+ ? (
+
+ ) : null}
+
+ )}
+ />
+ )
+ : (
+ // Fix eslint rule https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-fragments.md
+ <>
+ {trigger}
+ {content}
+ {showCloseBtn
+ ? (
+
+ ) : null}
+ >
+ )
+ }
+
+ );
+ }
+}
+
+Dropdown.propTypes = propTypes;
+Dropdown.defaultProps = defaultProps;
+
+Dropdown.DropdownTrigger = DropdownTrigger;
+Dropdown.DropdownContent = DropdownContent;
+Dropdown.DropdownList = DropdownList;
+Dropdown.DropdownListSeparator = DropdownListSeparator;
+Dropdown.DropdownListItem = DropdownListItem;
+Dropdown.DropdownListTitle = DropdownListTitle;
+export default injectIntl(Dropdown, { forwardRef: true });
diff --git a/src/2.7.12/imports/ui/components/dropdown/content/component.jsx b/src/2.7.12/imports/ui/components/dropdown/content/component.jsx
new file mode 100644
index 00000000..32d3225b
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/content/component.jsx
@@ -0,0 +1,41 @@
+import React, { Component, Children, cloneElement } from 'react';
+import Styled from './styles';
+
+const defaultProps = {
+ 'aria-expanded': false,
+};
+
+export default class DropdownContent extends Component {
+ render() {
+ const {
+ children,
+ dropdownToggle,
+ dropdownShow,
+ dropdownHide,
+ dropdownIsOpen,
+ keepOpen,
+ ...restProps
+ } = this.props;
+
+ const boundChildren = Children.map(children, child => cloneElement(child, {
+ dropdownIsOpen,
+ dropdownToggle,
+ dropdownShow,
+ dropdownHide,
+ keepOpen,
+ }));
+
+ return (
+
+
+ {boundChildren}
+
+
+ );
+ }
+}
+
+DropdownContent.defaultProps = defaultProps;
diff --git a/src/2.7.12/imports/ui/components/dropdown/content/styles.js b/src/2.7.12/imports/ui/components/dropdown/content/styles.js
new file mode 100644
index 00000000..ca962f40
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/content/styles.js
@@ -0,0 +1,154 @@
+import styled from 'styled-components';
+import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
+import {
+ borderSize,
+ borderRadius,
+ dropdownCaretHeight,
+ dropdownCaretWidth,
+} from '/imports/ui/stylesheets/styled-components/general';
+import { colorWhite, colorGray } from '/imports/ui/stylesheets/styled-components/palette';
+import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
+
+const Content = styled.div`
+ outline: transparent;
+ outline-width: ${borderSize};
+ outline-style: solid;
+ z-index: 9999;
+ position: absolute;
+ background: ${colorWhite};
+ border-radius: ${borderRadius};
+ box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
+ border: 0;
+ padding: calc(${lineHeightComputed} / 2);
+
+ [dir="rtl"] & {
+ right: 10.75rem;
+ }
+
+ &:after,
+ &:before {
+ content: '';
+ position: absolute;
+ width: 0;
+ height: 0;
+ }
+
+ &[aria-expanded="false"] {
+ display: none;
+ }
+
+ &[aria-expanded="true"] {
+ display: block;
+ }
+
+ @media ${smallOnly} {
+ z-index: 1015;
+ border-radius: 0;
+ background-color: #fff;
+ box-shadow: none;
+ position: fixed;
+ top: 0 !important;
+ left: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+ border: 0 !important;
+ padding: 0 !important;
+ margin: 0 0 calc(${lineHeightComputed} * 5.25) 0 !important;
+ transform: translateX(0) translateY(0) !important;
+
+ &:after,
+ &:before {
+ display: none !important;
+ }
+ }
+
+ //top-left
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-bottom: calc(${dropdownCaretHeight} * 1.25);
+
+ &:before,
+ &:after {
+ border-left: ${dropdownCaretWidth} solid transparent;
+ border-right: ${dropdownCaretWidth} solid transparent;
+ border-top: ${dropdownCaretHeight} solid ${colorWhite};
+ bottom: 0;
+ margin-bottom: calc(${dropdownCaretHeight} * -1);
+ }
+
+ &:before {
+ border-top: ${dropdownCaretHeight} solid ${colorGray};
+ }
+
+ transform: translateX(100%);
+ right: 100%;
+ left: auto;
+
+ &:after,
+ &:before {
+ left: ${dropdownCaretWidth};
+ }
+
+ min-width: 18rem;
+
+ @media ${smallOnly} {
+ width: auto;
+ }
+
+ [dir="rtl"] {
+ transform: translateX(25%);
+ }
+`;
+
+const Scrollable = styled.div`
+ @media ${smallOnly} {
+ overflow-y: auto;
+ background: linear-gradient(white 30%, rgba(255,255,255,0)),
+ linear-gradient(rgba(255,255,255,0), white 70%) 0 100%,
+ /* Shadows */
+ radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)),
+ radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%;
+
+ background-repeat: no-repeat;
+ background-color: transparent;
+ background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
+ background-attachment: local, local, scroll, scroll;
+
+ // Fancy scroll
+ &::-webkit-scrollbar {
+ width: 5px;
+ height: 5px;
+ }
+ &::-webkit-scrollbar-button {
+ width: 0;
+ height: 0;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: rgba(0,0,0,.25);
+ border: none;
+ border-radius: 50px;
+ }
+ &::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,.5); }
+ &::-webkit-scrollbar-thumb:active { background: rgba(0,0,0,.25); }
+ &::-webkit-scrollbar-track {
+ background: rgba(0,0,0,.25);
+ border: none;
+ border-radius: 50px;
+ }
+ &::-webkit-scrollbar-track:hover { background: rgba(0,0,0,.25); }
+ &::-webkit-scrollbar-track:active { background: rgba(0,0,0,.25); }
+ &::-webkit-scrollbar-corner { background: 0 0; }
+
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+`;
+
+export default {
+ Content,
+ Scrollable,
+};
diff --git a/src/2.7.12/imports/ui/components/dropdown/list/component.jsx b/src/2.7.12/imports/ui/components/dropdown/list/component.jsx
new file mode 100644
index 00000000..1497b410
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/list/component.jsx
@@ -0,0 +1,206 @@
+import React, { Component, Children, cloneElement } from 'react';
+import PropTypes from 'prop-types';
+import KEY_CODES from '/imports/utils/keyCodes';
+import Styled from './styles';
+import ListItem from './item/component';
+import ListSeparator from './separator/component';
+import ListTitle from './title/component';
+
+const propTypes = {
+ /* We should recheck this proptype, sometimes we need to create an container and send to
+ dropdown, but with this proptype, is not possible. */
+ children: PropTypes.arrayOf((propValue, key, componentName, propFullName) => {
+ if (propValue[key].type !== ListItem
+ && propValue[key].type !== ListSeparator
+ && propValue[key].type !== ListTitle) {
+ return new Error(`Invalid prop \`${propFullName}\` supplied to`
+ + ` \`${componentName}\`. Validation failed.`);
+ }
+ return true;
+ }).isRequired,
+
+ horizontal: PropTypes.bool,
+};
+
+const defaultProps = {
+ horizontal: false,
+};
+
+export default class DropdownList extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ focusedIndex: false,
+ };
+
+ this.childrenRefs = [];
+ this.menuRefs = [];
+ this.handleItemKeyDown = this.handleItemKeyDown.bind(this);
+ this.handleItemClick = this.handleItemClick.bind(this);
+ }
+
+ componentDidMount() {
+ this._menu.addEventListener('keydown', (event) => this.handleItemKeyDown(event));
+ }
+
+ componentDidUpdate() {
+ const { focusedIndex } = this.state;
+ const children = [].slice.call(this._menu.children);
+ this.menuRefs = children.filter((child) => child.getAttribute('role') === 'menuitem');
+
+ const activeRef = this.menuRefs[focusedIndex];
+
+ if (activeRef) {
+ activeRef.focus();
+ }
+ }
+
+ handleItemKeyDown(event, callback) {
+ const {
+ getDropdownMenuParent,
+ horizontal,
+ } = this.props;
+ const { focusedIndex } = this.state;
+
+ let nextFocusedIndex = focusedIndex > 0 ? focusedIndex : 0;
+
+ if (focusedIndex === false) {
+ nextFocusedIndex = this.menuRefs.indexOf(document.activeElement);
+ }
+
+ const isHorizontal = horizontal;
+ const navigationKeys = {
+ previous: KEY_CODES[`ARROW_${isHorizontal ? 'LEFT' : 'UP'}`],
+ next: KEY_CODES[`ARROW_${isHorizontal ? 'RIGHT' : 'DOWN'}`],
+ click: isHorizontal ? [KEY_CODES.ENTER] : [KEY_CODES.ENTER, KEY_CODES.ARROW_RIGHT],
+ close: [KEY_CODES.ESCAPE,
+ KEY_CODES.TAB,
+ KEY_CODES[`ARROW_${isHorizontal ? 'DOWN' : 'LEFT'}`]],
+ };
+
+ if (navigationKeys.previous === event.which) {
+ event.stopPropagation();
+
+ nextFocusedIndex -= 1;
+
+ if (nextFocusedIndex < 0) {
+ nextFocusedIndex = this.menuRefs.length - 1;
+ } else if (nextFocusedIndex > this.menuRefs.length - 1) {
+ nextFocusedIndex = 0;
+ }
+ }
+
+ if ([navigationKeys.next].includes(event.keyCode)) {
+ event.stopPropagation();
+
+ nextFocusedIndex += 1;
+
+ if (nextFocusedIndex > this.menuRefs.length - 1) {
+ nextFocusedIndex = 0;
+ }
+ }
+
+ if (navigationKeys.click.includes(event.keyCode)) {
+ nextFocusedIndex = false;
+ event.stopPropagation();
+ document.activeElement.firstChild.click();
+ }
+
+ if (navigationKeys.close.includes(event.keyCode)) {
+ nextFocusedIndex = false;
+ const { dropdownHide } = this.props;
+
+ event.stopPropagation();
+ event.preventDefault();
+
+ dropdownHide();
+ if (getDropdownMenuParent) {
+ getDropdownMenuParent().focus();
+ }
+ }
+
+ this.setState({ focusedIndex: nextFocusedIndex });
+
+ if (typeof callback === 'function') {
+ callback(event);
+ }
+ }
+
+ handleItemClick(event, callback) {
+ const {
+ getDropdownMenuParent,
+ onActionsHide,
+ dropdownHide,
+ keepOpen,
+ } = this.props;
+
+ if (!keepOpen) {
+ if (getDropdownMenuParent) {
+ onActionsHide();
+ } else {
+ this.setState({ focusedIndex: null });
+ dropdownHide();
+ }
+ }
+
+ if (typeof callback === 'function') {
+ callback(event);
+ }
+ }
+
+ render() {
+ const {
+ children,
+ style,
+ horizontal,
+ } = this.props;
+
+ const boundChildren = Children.map(
+ children,
+ (item) => {
+ if (item.type === ListSeparator) {
+ return item;
+ }
+
+ return cloneElement(item, {
+ tabIndex: 0,
+ injectRef: (ref) => {
+ if (ref && !this.childrenRefs.includes(ref)) { this.childrenRefs.push(ref); }
+ },
+
+ onClick: (event) => {
+ let { onClick } = item.props;
+ onClick = onClick ? onClick.bind(item) : null;
+ this.handleItemClick(event, onClick);
+ },
+
+ onKeyDown: (event) => {
+ let { onKeyDown } = item.props;
+ onKeyDown = onKeyDown ? onKeyDown.bind(item) : null;
+
+ this.handleItemKeyDown(event, onKeyDown);
+ },
+ });
+ },
+ );
+
+ const listDirection = horizontal ? 'horizontal' : 'vertical';
+
+ return (
+ {
+ this._menu = menu;
+ return menu;
+ }}
+ >
+ {boundChildren}
+
+ );
+ }
+}
+
+DropdownList.propTypes = propTypes;
+DropdownList.defaultProps = defaultProps;
diff --git a/src/2.7.12/imports/ui/components/dropdown/list/item/component.jsx b/src/2.7.12/imports/ui/components/dropdown/list/item/component.jsx
new file mode 100644
index 00000000..92446dbf
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/list/item/component.jsx
@@ -0,0 +1,103 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+import Styled from './styles';
+import { uniqueId } from '/imports/utils/string-utils';
+
+const propTypes = {
+ icon: PropTypes.string,
+ label: PropTypes.string,
+ description: PropTypes.string,
+ accessKey: PropTypes.string,
+ tabIndex: PropTypes.number,
+ disabled: PropTypes.bool,
+};
+
+const defaultProps = {
+ icon: '',
+ label: '',
+ description: '',
+ tabIndex: 0,
+ accessKey: null,
+ disabled: false,
+};
+
+const messages = defineMessages({
+ activeAriaLabel: {
+ id: 'app.dropdown.list.item.activeLabel',
+ },
+});
+
+class DropdownListItem extends Component {
+ constructor(props) {
+ super(props);
+ this.labelID = uniqueId('dropdown-item-label-');
+ this.descID = uniqueId('dropdown-item-desc-');
+ }
+
+ renderDefault() {
+ const {
+ icon, label, iconRight, accessKey,
+ } = this.props;
+
+ return [
+ (icon ? : null),
+ (
+
+ {label}
+
+ ),
+ (iconRight ? : null),
+ ];
+ }
+
+ render() {
+ const {
+ id,
+ label,
+ description,
+ children,
+ injectRef,
+ tabIndex,
+ onClick,
+ onKeyDown,
+ className,
+ style,
+ intl,
+ disabled,
+ 'data-test': dataTest,
+ } = this.props;
+
+ const isSelected = className && className.includes('emojiSelected');
+ const _label = isSelected ? `${label} (${intl.formatMessage(messages.activeAriaLabel)})` : label;
+ return (
+ {} : onClick}
+ onKeyDown={disabled ? () => {} : onKeyDown}
+ tabIndex={tabIndex}
+ aria-labelledby={this.labelID}
+ aria-describedby={this.descID}
+ style={style}
+ role="menuitem"
+ data-test={dataTest}
+ >
+ {
+ children || this.renderDefault()
+ }
+ {
+ label
+ ? ({_label} )
+ : null
+ }
+ {description}
+
+ );
+ }
+}
+
+export default injectIntl(DropdownListItem);
+
+DropdownListItem.propTypes = propTypes;
+DropdownListItem.defaultProps = defaultProps;
diff --git a/src/2.7.12/imports/ui/components/dropdown/list/item/styles.js b/src/2.7.12/imports/ui/components/dropdown/list/item/styles.js
new file mode 100644
index 00000000..f5b483bb
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/list/item/styles.js
@@ -0,0 +1,99 @@
+import styled from 'styled-components';
+import Icon from '/imports/ui/components/common/icon/component';
+import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
+import {
+ colorGrayDark,
+ colorPrimary,
+ colorWhite,
+ colorText,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import { borderSize } from '/imports/ui/stylesheets/styled-components/general';
+import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
+
+const ItemIcon = styled(Icon)`
+ margin: 0 calc(${lineHeightComputed} / 2) 0 0;
+ color: ${colorText};
+ flex: 0 0;
+
+ [dir="rtl"] & {
+ margin: 0 0 0 calc(${lineHeightComputed} / 2);
+ }
+`;
+
+const ItemLabel = styled.span`
+ color: ${colorGrayDark};
+ font-size: 90%;
+ flex: 1;
+`;
+
+const IconRight = styled(ItemIcon)`
+ margin-right: 0;
+ margin-left: 1rem;
+ font-size: 12px;
+ line-height: 16px;
+
+ [dir="rtl"] & {
+ margin-left: 0;
+ margin-right: 1rem;
+ -webkit-transform: scale(-1, 1);
+ -moz-transform: scale(-1, 1);
+ -ms-transform: scale(-1, 1);
+ -o-transform: scale(-1, 1);
+ transform: scale(-1, 1);
+ }
+`;
+
+const Item = styled.li`
+ display: flex;
+ flex: 1 1 100%;
+ align-items: center;
+ padding: calc(${lineHeightComputed} / 3) 0;
+
+ &:hover,
+ &:focus {
+ outline: transparent;
+ outline-style: dotted;
+ outline-width: ${borderSize};
+
+ cursor: pointer;
+ background-color: ${colorPrimary};
+ color: ${colorWhite};
+
+ & > span {
+ color: ${colorWhite} !important;
+ }
+
+ margin-left: -.25rem;
+ margin-right: -.25rem;
+ padding-left: .25rem;
+ padding-right: .25rem;
+
+ [dir="rtl"] & {
+ margin-right: -.25rem;
+ margin-left: -.25rem;
+ padding-right: .25rem;
+ padding-left: .25rem;
+ }
+
+
+ @media ${smallOnly} {
+ border-radius: 0.2rem;
+ }
+
+ & i {
+ color: inherit;
+ }
+ }
+
+ &:focus {
+ box-shadow: 0 0 0 2px ${colorWhite}, 0 0 2px 4px rgba(${colorPrimary}, .4);
+ outline-style: solid;
+ }
+`;
+
+export default {
+ ItemIcon,
+ ItemLabel,
+ IconRight,
+ Item,
+};
diff --git a/src/2.7.12/imports/ui/components/dropdown/list/separator/component.jsx b/src/2.7.12/imports/ui/components/dropdown/list/separator/component.jsx
new file mode 100644
index 00000000..31957b03
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/list/separator/component.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Styled from './styles';
+
+const DropdownListSeparator = ({ style }) => (
+
+);
+
+DropdownListSeparator.propTypes = {
+ style: PropTypes.shape({}),
+};
+
+DropdownListSeparator.defaultProps = {
+ style: null,
+};
+
+export default DropdownListSeparator;
diff --git a/src/2.7.12/imports/ui/components/dropdown/list/separator/styles.js b/src/2.7.12/imports/ui/components/dropdown/list/separator/styles.js
new file mode 100644
index 00000000..26279a0d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/list/separator/styles.js
@@ -0,0 +1,18 @@
+import styled from 'styled-components';
+import { colorGrayLighter } from '/imports/ui/stylesheets/styled-components/palette';
+import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
+
+const Separator = styled.li`
+ display: flex;
+ flex: 1 1 100%;
+ height: 1px;
+ min-height: 1px;
+ background-color: ${colorGrayLighter};
+ padding: 0;
+ margin-top: calc(${lineHeightComputed} * .5);
+ margin-bottom: calc(${lineHeightComputed} * .5);
+`;
+
+export default {
+ Separator,
+};
diff --git a/src/2.7.12/imports/ui/components/dropdown/list/styles.js b/src/2.7.12/imports/ui/components/dropdown/list/styles.js
new file mode 100644
index 00000000..e7a5f63f
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/list/styles.js
@@ -0,0 +1,47 @@
+import styled from 'styled-components';
+import { fontSizeBase, fontSizeLarge, lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
+import { colorGrayDark } from '/imports/ui/stylesheets/styled-components/palette';
+
+import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
+
+const List = styled.ul`
+ list-style: none;
+ font-size: ${fontSizeBase};
+ margin: 0;
+ padding: 0;
+ text-align: left;
+ color: ${colorGrayDark};
+ display: flex;
+ overflow-wrap: break-word;
+ white-space: pre-line;
+
+ [dir="rtl"] & {
+ text-align: right;
+ }
+
+ @media ${smallOnly} {
+ font-size: calc(${fontSizeLarge} * 1.1);
+ padding: ${lineHeightComputed};
+ }
+
+ ${({ direction }) => direction === 'horizontal' && `
+ padding: 0;
+ flex-direction: row;
+
+ @media ${smallOnly} {
+ flex-direction: column;
+ padding: calc(${lineHeightComputed} / 1.5) 0;
+ }
+
+ padding: 0 calc(${lineHeightComputed} / 3);
+ `}
+
+ ${({ direction }) => direction === 'vertical' && `
+ flex-direction: column;
+ width: 100%;
+ `}
+`;
+
+export default {
+ List,
+};
diff --git a/src/2.7.12/imports/ui/components/dropdown/list/title/component.jsx b/src/2.7.12/imports/ui/components/dropdown/list/title/component.jsx
new file mode 100644
index 00000000..7b3947a0
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/list/title/component.jsx
@@ -0,0 +1,22 @@
+import React, { Component } from 'react';
+import Styled from './styles';
+import { uniqueId } from '/imports/utils/string-utils';
+
+export default class DropdownListTitle extends Component {
+ constructor(props) {
+ super(props);
+ this.labelID = uniqueId('labelContext-');
+ }
+
+ render() {
+ const {
+ children,
+ } = this.props;
+
+ return (
+
+ {children}
+
+ );
+ }
+}
diff --git a/src/2.7.12/imports/ui/components/dropdown/list/title/styles.js b/src/2.7.12/imports/ui/components/dropdown/list/title/styles.js
new file mode 100644
index 00000000..a0227152
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/list/title/styles.js
@@ -0,0 +1,12 @@
+import styled from 'styled-components';
+import { colorGray } from '/imports/ui/stylesheets/styled-components/palette';
+
+const Title = styled.li`
+ color: ${colorGray};
+ font-weight: 600;
+ width: 100%;
+`;
+
+export default {
+ Title,
+};
diff --git a/src/2.7.12/imports/ui/components/dropdown/styles.js b/src/2.7.12/imports/ui/components/dropdown/styles.js
new file mode 100644
index 00000000..b7fcdd77
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/styles.js
@@ -0,0 +1,37 @@
+import styled from 'styled-components';
+import Button from '/imports/ui/components/common/button/component';
+import { fontSizeLarge } from '/imports/ui/stylesheets/styled-components/typography';
+import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
+import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
+import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
+
+const Dropdown = styled.div`
+ position: relative;
+ z-index: 3;
+
+ &:focus {
+ outline: none;
+ }
+`;
+
+const CloseButton = styled(Button)`
+ display: none;
+ position: fixed;
+ bottom: 0.8rem;
+ border-radius: 0;
+ z-index: 1011;
+ font-size: calc(${fontSizeLarge} * 1.1);
+ width: calc(100% - (${lineHeightComputed} * 2));
+ left: ${lineHeightComputed};
+ box-shadow: 0 0 0 2rem ${colorWhite} !important;
+ border: ${colorWhite} !important;
+
+ @media ${smallOnly} {
+ display: block;
+ }
+`;
+
+export default {
+ Dropdown,
+ CloseButton,
+};
diff --git a/src/2.7.12/imports/ui/components/dropdown/trigger/component.jsx b/src/2.7.12/imports/ui/components/dropdown/trigger/component.jsx
new file mode 100644
index 00000000..904e1437
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/dropdown/trigger/component.jsx
@@ -0,0 +1,101 @@
+import React, { Component } from 'react';
+import Button from '/imports/ui/components/common/button/component';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+import KEY_CODES from '/imports/utils/keyCodes';
+
+const propTypes = {
+ children: PropTypes.element.isRequired,
+};
+
+export default class DropdownTrigger extends Component {
+ static isButtonTriggerOnEmoji(buttonComponent) {
+ return (
+ buttonComponent
+ && (buttonComponent.type === Button)
+ && (buttonComponent.props)
+ && (buttonComponent.props.children)
+ );
+ }
+
+ constructor(props) {
+ super(props);
+ this.handleClick = this.handleClick.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.trigger = null;
+ }
+
+ handleClick(e) {
+ e.stopPropagation();
+ const { dropdownToggle, onClick } = this.props;
+ if (onClick) onClick();
+ return dropdownToggle();
+ }
+
+ handleKeyDown(event) {
+ event.stopPropagation();
+ const { dropdownShow, dropdownHide } = this.props;
+
+ if ([KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.which)) {
+ event.preventDefault();
+ event.stopPropagation();
+ } else if ([KEY_CODES.ARROW_UP, KEY_CODES.ARROW_DOWN].includes(event.which)) {
+ dropdownShow();
+ } else if (KEY_CODES.ESCAPE === event.which) {
+ dropdownHide();
+ }
+ }
+
+ render() {
+ const { dropdownIsOpen } = this.props;
+ const remainingProps = { ...this.props };
+ delete remainingProps.dropdownToggle;
+ delete remainingProps.dropdownShow;
+ delete remainingProps.dropdownHide;
+ delete remainingProps.dropdownIsOpen;
+
+ const {
+ children,
+ className,
+ ...restProps
+ } = remainingProps;
+
+ let TriggerComponent;
+ let TriggerComponentBounded;
+
+ const buttonComponentProps = {
+ ...restProps,
+ 'aria-expanded': dropdownIsOpen,
+ };
+
+ const triggerComponentProps = {
+ onClick: this.handleClick,
+ onKeyDown: this.handleKeyDown,
+ };
+
+ if (DropdownTrigger.isButtonTriggerOnEmoji(children)) {
+ const { children: grandChildren } = children.props;
+
+ triggerComponentProps.className = cx(children.props.className, className);
+
+ TriggerComponent = React.Children.only(grandChildren);
+ TriggerComponentBounded = React.cloneElement(TriggerComponent,
+ triggerComponentProps);
+
+ const ButtonComponent = React.Children.only(children);
+ return React.cloneElement(ButtonComponent,
+ buttonComponentProps, TriggerComponentBounded);
+ }
+
+ buttonComponentProps.className = cx(children.props.className, className);
+ TriggerComponent = React.Children.only(children);
+
+ TriggerComponentBounded = React.cloneElement(TriggerComponent,
+ { ...buttonComponentProps, ...triggerComponentProps });
+
+ return TriggerComponentBounded;
+ }
+}
+
+DropdownTrigger.propTypes = propTypes;
diff --git a/src/2.7.12/imports/ui/components/emoji-picker/component.jsx b/src/2.7.12/imports/ui/components/emoji-picker/component.jsx
new file mode 100644
index 00000000..0b3b015d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/emoji-picker/component.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl } from 'react-intl';
+import data from '@emoji-mart/data';
+import Picker from '@emoji-mart/react';
+
+const DISABLE_EMOJIS = Meteor.settings.public.chat.disableEmojis;
+
+const propTypes = {
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func.isRequired,
+ }).isRequired,
+ onEmojiSelect: PropTypes.func.isRequired,
+};
+
+const emojisToExclude = [
+ ...DISABLE_EMOJIS,
+];
+
+const EmojiPicker = (props) => {
+ const {
+ intl,
+ onEmojiSelect,
+ } = props;
+
+ const i18n = {
+ search: intl.formatMessage({ id: 'app.emojiPicker.search' }),
+ notfound: intl.formatMessage({ id: 'app.emojiPicker.notFound' }),
+ clear: intl.formatMessage({ id: 'app.emojiPicker.clear' }),
+ skintext: intl.formatMessage({ id: 'app.emojiPicker.skintext' }),
+ categories: {
+ people: intl.formatMessage({ id: 'app.emojiPicker.categories.people' }),
+ nature: intl.formatMessage({ id: 'app.emojiPicker.categories.nature' }),
+ foods: intl.formatMessage({ id: 'app.emojiPicker.categories.foods' }),
+ places: intl.formatMessage({ id: 'app.emojiPicker.categories.places' }),
+ activity: intl.formatMessage({ id: 'app.emojiPicker.categories.activity' }),
+ objects: intl.formatMessage({ id: 'app.emojiPicker.categories.objects' }),
+ symbols: intl.formatMessage({ id: 'app.emojiPicker.categories.symbols' }),
+ flags: intl.formatMessage({ id: 'app.emojiPicker.categories.flags' }),
+ recent: intl.formatMessage({ id: 'app.emojiPicker.categories.recent' }),
+ search: intl.formatMessage({ id: 'app.emojiPicker.categories.search' }),
+ },
+ categorieslabel: intl.formatMessage({ id: 'app.emojiPicker.categories.label' }),
+ skintones: {
+ 1: intl.formatMessage({ id: 'app.emojiPicker.skintones.1' }),
+ 2: intl.formatMessage({ id: 'app.emojiPicker.skintones.2' }),
+ 3: intl.formatMessage({ id: 'app.emojiPicker.skintones.3' }),
+ 4: intl.formatMessage({ id: 'app.emojiPicker.skintones.4' }),
+ 5: intl.formatMessage({ id: 'app.emojiPicker.skintones.5' }),
+ 6: intl.formatMessage({ id: 'app.emojiPicker.skintones.6' }),
+ },
+ };
+
+ return (
+ onEmojiSelect(emojiObject, event)}
+ emojiSize={24}
+ i18n={i18n}
+ previewPosition="none"
+ skinTonePosition="none"
+ theme="light"
+ dynamicWidth
+ exceptEmojis={emojisToExclude}
+ />
+ );
+};
+
+EmojiPicker.propTypes = propTypes;
+
+export default injectIntl(EmojiPicker);
diff --git a/src/2.7.12/imports/ui/components/emoji-picker/reactions-picker/styles.js b/src/2.7.12/imports/ui/components/emoji-picker/reactions-picker/styles.js
new file mode 100644
index 00000000..fda0dd7d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/emoji-picker/reactions-picker/styles.js
@@ -0,0 +1,35 @@
+import styled, { css } from 'styled-components';
+
+const EmojiWrapper = styled.span`
+ padding-top: 0.9em;
+ padding-bottom: 0.1em;
+ ${({ selected }) => !selected && css`
+ :hover {
+ border-radius:100%;
+ outline-color: transparent;
+ outline-style:solid;
+ box-shadow: 0 0 0 0.25em #eee;
+ background-color: #eee;
+ opacity: 0.75;
+ }
+ `}
+ ${({ selected }) => selected && css`
+ em-emoji {
+ cursor: not-allowed;
+ }
+ `}
+ ${({ selected, emoji }) => selected && selected !== emoji && css`
+ opacity: 0.75;
+ `}
+ ${({ selected, emoji }) => selected && selected === emoji && css`
+ border-radius:100%;
+ outline-color: transparent;
+ outline-style:solid;
+ box-shadow: 0 0 0 0.25em #eee;
+ background-color: #eee;
+ `}
+`;
+
+export default {
+ EmojiWrapper,
+};
diff --git a/src/2.7.12/imports/ui/components/emoji-picker/reactions-picker/styles.scss b/src/2.7.12/imports/ui/components/emoji-picker/reactions-picker/styles.scss
new file mode 100644
index 00000000..4b33f6de
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/emoji-picker/reactions-picker/styles.scss
@@ -0,0 +1,27 @@
+.dropdownContent {
+ section[class^="emoji-mart-search"] {
+ display: none !important;
+ }
+ section[class^="emoji-mart emoji-mart-light"] {
+ display: unset;
+ border: unset;
+ }
+ div[class^="emoji-mart-bar"] {
+ display: none !important;
+ }
+ section[aria-label^="Frequently Used"] {
+ display: none !important;
+ }
+ div[class^="emoji-mart-category-label"] {
+ display: none !important;
+ }
+ div[class^="emoji-mart-scroll"] {
+ overflow-y: unset;
+ height: unset;
+ }
+ ul[class^="emoji-mart-category-list"] {
+ span {
+ cursor: pointer !important;
+ }
+ }
+}
diff --git a/src/2.7.12/imports/ui/components/emoji-rain/component.jsx b/src/2.7.12/imports/ui/components/emoji-rain/component.jsx
new file mode 100644
index 00000000..fed1c286
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/emoji-rain/component.jsx
@@ -0,0 +1,121 @@
+import React, { useRef, useState, useEffect } from 'react';
+import Settings from '/imports/ui/services/settings';
+import Service from './service';
+import logger from '/imports/startup/client/logger';
+
+const EmojiRain = ({ reactions }) => {
+ const containerRef = useRef(null);
+ const [isAnimating, setIsAnimating] = useState(false);
+ const EMOJI_SIZE = Meteor.settings.public.app.emojiRain.emojiSize;
+ const NUMBER_OF_EMOJIS = Meteor.settings.public.app.emojiRain.numberOfEmojis;
+ const EMOJI_RAIN_ENABLED = Meteor.settings.public.app.emojiRain.enabled;
+
+ const { animations } = Settings.application;
+
+ function createEmojiRain(emoji) {
+ const coord = Service.getInteractionsButtonCoordinates();
+ const flyingEmojis = [];
+
+ if (coord == null) {
+ logger.warn({
+ logCode: 'interactions_emoji_rain_no_coord',
+ }, 'No coordinates for interactions button, skipping emoji rain');
+ return;
+ }
+
+ for (i = 0; i < NUMBER_OF_EMOJIS; i++) {
+ const initialPosition = {
+ x: coord.x + coord.width / 8,
+ y: coord.y + coord.height / 5,
+ };
+ const endPosition = {
+ x: Math.floor(Math.random() * 100) + coord.x - 100 / 2,
+ y: Math.floor(Math.random() * 300) + coord.y / 2,
+ };
+ const scale = Math.floor(Math.random() * (8 - 4 + 1)) - 40;
+ const sec = Math.floor(Math.random() * 1700) + 2000;
+
+ const shapeElement = document.createElement('svg');
+ const emojiElement = document.createElement('text');
+ emojiElement.setAttribute('x', '50%');
+ emojiElement.setAttribute('y', '50%');
+ emojiElement.innerHTML = emoji;
+
+ shapeElement.style.position = 'absolute';
+ shapeElement.style.left = `${initialPosition.x}px`;
+ shapeElement.style.top = `${initialPosition.y}px`;
+ shapeElement.style.transform = `scaleX(0.${scale}) scaleY(0.${scale})`;
+ shapeElement.style.transition = `${sec}ms`;
+ shapeElement.style.fontSize = `${EMOJI_SIZE}em`;
+ shapeElement.style.pointerEvents = 'none';
+
+ shapeElement.appendChild(emojiElement);
+ containerRef.current.appendChild(shapeElement);
+
+ flyingEmojis.push({ shapeElement, endPosition });
+ }
+
+ requestAnimationFrame(() => setTimeout(() => flyingEmojis.forEach((emoji) => {
+ const { shapeElement, endPosition } = emoji;
+ shapeElement.style.left = `${endPosition.x}px`;
+ shapeElement.style.top = `${endPosition.y}px`;
+ shapeElement.style.transform = 'scaleX(0) scaleY(0)';
+ }), 0));
+
+ setTimeout(() => {
+ flyingEmojis.forEach((emoji) => emoji.shapeElement.remove());
+ flyingEmojis.length = 0;
+ }, 2000);
+ }
+
+ const handleVisibilityChange = () => {
+ if (document.hidden) {
+ setIsAnimating(false);
+ } else if (EMOJI_RAIN_ENABLED && animations) {
+ setIsAnimating(true);
+ }
+ };
+
+ useEffect(() => {
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+
+ return () => {
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (EMOJI_RAIN_ENABLED && animations && !isAnimating && !document.hidden) {
+ setIsAnimating(true);
+ } else if (!animations) {
+ setIsAnimating(false);
+ }
+ }, [EMOJI_RAIN_ENABLED, animations, isAnimating]);
+
+ useEffect(() => {
+ if (isAnimating && animations !== false) {
+ reactions.forEach((reaction) => {
+ const currentTime = new Date().getTime();
+ const secondsSinceCreated = (currentTime - reaction.creationDate.getTime()) / 1000;
+ if (secondsSinceCreated <= 1 && (reaction.reaction !== 'none')) {
+ createEmojiRain(reaction.reaction);
+ }
+ });
+ }
+ }, [isAnimating, reactions]);
+
+ const containerStyle = {
+ width: '100vw',
+ height: '100vh',
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ overflow: 'hidden',
+ pointerEvents: 'none',
+ zIndex: 2,
+ };
+
+ return
;
+};
+
+export default EmojiRain;
diff --git a/src/2.7.12/imports/ui/components/emoji-rain/container.jsx b/src/2.7.12/imports/ui/components/emoji-rain/container.jsx
new file mode 100644
index 00000000..d6f24ef7
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/emoji-rain/container.jsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import EmojiRain from './component';
+import UserReaction from '/imports/api/user-reaction';
+import Auth from '/imports/ui/services/auth';
+
+const EmojiRainContainer = (props) => ;
+
+export default withTracker(() => ({
+ reactions: UserReaction.find({ meetingId: Auth.meetingID }).fetch(),
+}))(EmojiRainContainer);
diff --git a/src/2.7.12/imports/ui/components/emoji-rain/service.js b/src/2.7.12/imports/ui/components/emoji-rain/service.js
new file mode 100644
index 00000000..b172559c
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/emoji-rain/service.js
@@ -0,0 +1,13 @@
+const getInteractionsButtonCoordinates = () => {
+ const el = document.getElementById('interactionsButton');
+
+ if (!el) return null;
+
+ const coordinate = el.getBoundingClientRect();
+
+ return coordinate;
+};
+
+export default {
+ getInteractionsButtonCoordinates,
+};
diff --git a/src/2.7.12/imports/ui/components/end-meeting-confirmation/component.jsx b/src/2.7.12/imports/ui/components/end-meeting-confirmation/component.jsx
new file mode 100644
index 00000000..47c8ce5b
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/end-meeting-confirmation/component.jsx
@@ -0,0 +1,80 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+import ConfirmationModal from '/imports/ui/components/common/modal/confirmation/component';
+
+const intlMessages = defineMessages({
+ endMeetingTitle: {
+ id: 'app.endMeeting.title',
+ description: 'end meeting title',
+ },
+ endMeetingDescription: {
+ id: 'app.endMeeting.description',
+ description: 'end meeting description with affected users information',
+ },
+ endMeetingNoUserDescription: {
+ id: 'app.endMeeting.noUserDescription',
+ description: 'end meeting description',
+ },
+ contentWarning: {
+ id: 'app.endMeeting.contentWarning',
+ description: 'end meeting content warning',
+ },
+ confirmButtonLabel: {
+ id: 'app.endMeeting.yesLabel',
+ description: 'end meeting confirm button label',
+ },
+});
+
+const { warnAboutUnsavedContentOnMeetingEnd } = Meteor.settings.public.app;
+
+const propTypes = {
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func.isRequired,
+ }).isRequired,
+ endMeeting: PropTypes.func.isRequired,
+ meetingTitle: PropTypes.string.isRequired,
+ users: PropTypes.number.isRequired,
+};
+
+class EndMeetingComponent extends PureComponent {
+ render() {
+ const {
+ users, intl, endMeeting, meetingTitle,
+ isOpen, onRequestClose, priority, setIsOpen,
+ } = this.props;
+
+ const title = intl.formatMessage(intlMessages.endMeetingTitle, { 0: meetingTitle });
+
+ let description = users > 0
+ ? intl.formatMessage(intlMessages.endMeetingDescription, { 0: users })
+ : intl.formatMessage(intlMessages.endMeetingNoUserDescription);
+
+ if (warnAboutUnsavedContentOnMeetingEnd) {
+ // the double breakline it to put one empty line between the descriptions
+ description += `\n\n${intl.formatMessage(intlMessages.contentWarning)}`;
+ }
+
+ return (
+
+ );
+ }
+}
+
+EndMeetingComponent.propTypes = propTypes;
+
+export default injectIntl(EndMeetingComponent);
diff --git a/src/2.7.12/imports/ui/components/end-meeting-confirmation/container.jsx b/src/2.7.12/imports/ui/components/end-meeting-confirmation/container.jsx
new file mode 100644
index 00000000..51e8b98f
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/end-meeting-confirmation/container.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import { makeCall } from '/imports/ui/services/api';
+import EndMeetingComponent from './component';
+import Service from './service';
+import logger from '/imports/startup/client/logger';
+
+const EndMeetingContainer = (props) => ;
+
+export default withTracker((props) => ({
+ endMeeting: () => {
+ logger.warn({
+ logCode: 'moderator_forcing_end_meeting',
+ extraInfo: { logType: 'user_action' },
+ }, 'this user clicked on EndMeeting and confirmed, removing everybody from the meeting');
+ makeCall('endMeeting');
+ props.setIsOpen(false);
+ },
+ meetingTitle: Service.getMeetingTitle(),
+ users: Service.getUsers(),
+}))(EndMeetingContainer);
diff --git a/src/2.7.12/imports/ui/components/end-meeting-confirmation/service.js b/src/2.7.12/imports/ui/components/end-meeting-confirmation/service.js
new file mode 100644
index 00000000..5817cdab
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/end-meeting-confirmation/service.js
@@ -0,0 +1,25 @@
+import Users from '/imports/api/users';
+import Auth from '/imports/ui/services/auth';
+import Meetings from '/imports/api/meetings';
+
+const getMeetingTitle = () => {
+ const meeting = Meetings.findOne({
+ meetingId: Auth.meetingID,
+ }, { fields: { 'meetingProp.name': 1, 'breakoutProps.sequence': 1 } });
+
+ return meeting.meetingProp.name;
+};
+
+const getUsers = () => {
+ const numUsers = Users.find({
+ meetingId: Auth.meetingID,
+ userId: { $ne: Auth.userID },
+ }, { fields: { _id: 1 } }).count();
+
+ return numUsers;
+};
+
+export default {
+ getUsers,
+ getMeetingTitle,
+};
diff --git a/src/2.7.12/imports/ui/components/error-screen/component.jsx b/src/2.7.12/imports/ui/components/error-screen/component.jsx
new file mode 100644
index 00000000..15cb0f37
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/error-screen/component.jsx
@@ -0,0 +1,149 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+import { Meteor } from 'meteor/meteor';
+import { Session } from 'meteor/session';
+import AudioManager from '/imports/ui/services/audio-manager';
+import logger from '/imports/startup/client/logger';
+import Styled from './styles';
+
+const intlMessages = defineMessages({
+ 503: {
+ id: 'app.error.503',
+ },
+ 500: {
+ id: 'app.error.500',
+ defaultMessage: 'Oops, something went wrong',
+ },
+ 410: {
+ id: 'app.error.410',
+ },
+ 409: {
+ id: 'app.error.409',
+ },
+ 408: {
+ id: 'app.error.408',
+ },
+ 404: {
+ id: 'app.error.404',
+ defaultMessage: 'Not found',
+ },
+ 403: {
+ id: 'app.error.403',
+ },
+ 401: {
+ id: 'app.error.401',
+ },
+ 400: {
+ id: 'app.error.400',
+ },
+ meeting_ended: {
+ id: 'app.meeting.endedMessage',
+ },
+ user_logged_out_reason: {
+ id: 'app.error.userLoggedOut',
+ },
+ validate_token_failed_eject_reason: {
+ id: 'app.error.ejectedUser',
+ },
+ banned_user_rejoining_reason: {
+ id: 'app.error.userBanned',
+ },
+ joined_another_window_reason: {
+ id: 'app.error.joinedAnotherWindow',
+ },
+ user_inactivity_eject_reason: {
+ id: 'app.meeting.logout.userInactivityEjectReason',
+ },
+ user_requested_eject_reason: {
+ id: 'app.meeting.logout.ejectedFromMeeting',
+ },
+ max_participants_reason: {
+ id: 'app.meeting.logout.maxParticipantsReached',
+ },
+ guest_deny: {
+ id: 'app.guest.guestDeny',
+ },
+ duplicate_user_in_meeting_eject_reason: {
+ id: 'app.meeting.logout.duplicateUserEjectReason',
+ },
+ not_enough_permission_eject_reason: {
+ id: 'app.meeting.logout.permissionEjectReason',
+ },
+ able_to_rejoin_user_disconnected_reason: {
+ id: 'app.error.disconnected.rejoin',
+ },
+});
+
+const propTypes = {
+ code: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+};
+
+const defaultProps = {
+ code: '500',
+ callback: async () => {},
+};
+
+class ErrorScreen extends PureComponent {
+ componentDidMount() {
+ const { code, callback } = this.props;
+ const log = code === '403' ? 'warn' : 'error';
+ AudioManager.exitAudio();
+ callback().finally(() => {
+ Meteor.disconnect();
+ });
+ logger[log]({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${code}`);
+ }
+
+ render() {
+ const {
+ intl,
+ code,
+ children,
+ } = this.props;
+
+ let formatedMessage = intl.formatMessage(intlMessages[defaultProps.code]);
+
+ if (code in intlMessages) {
+ formatedMessage = intl.formatMessage(intlMessages[code]);
+ }
+
+ let errorMessageDescription = Session.get('errorMessageDescription');
+
+ if (errorMessageDescription in intlMessages) {
+ errorMessageDescription = intl.formatMessage(intlMessages[errorMessageDescription]);
+ }
+
+ return (
+
+
+ {formatedMessage}
+
+ {
+ !errorMessageDescription
+ || formatedMessage === errorMessageDescription
+ || (
+
+ {errorMessageDescription}
+
+ )
+ }
+
+
+ {code}
+
+
+ {children}
+
+
+ );
+ }
+}
+
+export default injectIntl(ErrorScreen);
+
+ErrorScreen.propTypes = propTypes;
+ErrorScreen.defaultProps = defaultProps;
diff --git a/src/2.7.12/imports/ui/components/error-screen/styles.js b/src/2.7.12/imports/ui/components/error-screen/styles.js
new file mode 100644
index 00000000..7554699a
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/error-screen/styles.js
@@ -0,0 +1,57 @@
+import styled from 'styled-components';
+import {
+ colorWhite,
+ colorGrayDark,
+ colorGrayLighter,
+} from '/imports/ui/stylesheets/styled-components/palette';
+
+const Background = styled.div`
+ position: fixed;
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ background-color: ${colorGrayDark};
+ color: ${colorWhite};
+ text-align: center;
+`;
+
+const Message = styled.h1`
+ margin: 0;
+ color: ${colorWhite};
+ font-size: 1.75rem;
+ font-weight: 400;
+ margin-bottom: 1rem;
+`;
+
+const SessionMessage = styled.div`
+ margin: 0;
+ color: ${colorWhite};
+ font-weight: 400;
+ margin-bottom: 1rem;
+ font-size: 1.25rem;
+`;
+
+const Separator = styled.div`
+ height: 0;
+ width: 5rem;
+ border: 1px solid ${colorGrayLighter};
+ margin: 1.5rem 0 1.5rem 0;
+ align-self: center;
+ opacity: .75;
+`;
+
+const CodeError = styled.h1`
+ margin: 0;
+ font-size: 1.5rem;
+ color: ${colorWhite};
+`;
+
+export default {
+ Background,
+ Message,
+ SessionMessage,
+ Separator,
+ CodeError,
+};
diff --git a/src/2.7.12/imports/ui/components/external-video-player/component.jsx b/src/2.7.12/imports/ui/components/external-video-player/component.jsx
new file mode 100644
index 00000000..d1cd24fa
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/component.jsx
@@ -0,0 +1,728 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
+import { defineMessages, injectIntl } from 'react-intl';
+import {
+ sendMessage,
+ onMessage,
+ removeAllListeners,
+ getPlayingState,
+} from './service';
+import deviceInfo from '/imports/utils/deviceInfo';
+
+import logger from '/imports/startup/client/logger';
+
+import Subtitles from './subtitles/component';
+import VolumeSlider from './volume-slider/component';
+import ReloadButton from '/imports/ui/components/reload-button/component';
+import FullscreenButtonContainer from '/imports/ui/components/common/fullscreen-button/container';
+
+import ArcPlayer from '/imports/ui/components/external-video-player/custom-players/arc-player';
+import PeerTubePlayer from '/imports/ui/components/external-video-player/custom-players/peertube';
+import { ACTIONS } from '/imports/ui/components/layout/enums';
+import { uniqueId } from '/imports/utils/string-utils';
+import { throttle } from 'radash';
+
+import Styled from './styles';
+
+const intlMessages = defineMessages({
+ autoPlayWarning: {
+ id: 'app.externalVideo.autoPlayWarning',
+ description: 'Shown when user needs to interact with player to make it work',
+ },
+ refreshLabel: {
+ id: 'app.externalVideo.refreshLabel',
+ },
+ fullscreenLabel: {
+ id: 'app.externalVideo.fullscreenLabel',
+ },
+ subtitlesOn: {
+ id: 'app.externalVideo.subtitlesOn',
+ },
+ subtitlesOff: {
+ id: 'app.externalVideo.subtitlesOff',
+ },
+});
+
+const SYNC_INTERVAL_SECONDS = 5;
+const THROTTLE_INTERVAL_SECONDS = 0.5;
+const AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS = 5;
+const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
+const THROTTLE_RELOAD_INTERVAL = 5000;
+
+Styled.VideoPlayer.addCustomPlayer(PeerTubePlayer);
+Styled.VideoPlayer.addCustomPlayer(ArcPlayer);
+
+class VideoPlayer extends Component {
+ static clearVideoListeners() {
+ removeAllListeners('play');
+ removeAllListeners('stop');
+ removeAllListeners('playerUpdate');
+ removeAllListeners('presenterReady');
+ }
+
+ constructor(props) {
+ super(props);
+
+ const { isPresenter } = props;
+
+ this.player = null;
+ this.syncInterval = null;
+ this.autoPlayTimeout = null;
+ this.hasPlayedBefore = false;
+ this.playerIsReady = false;
+
+ this.lastMessage = null;
+ this.lastMessageTimestamp = Date.now();
+
+ this.throttleTimeout = null;
+
+ this.state = {
+ subtitlesOn: false,
+ muted: false,
+ playing: false,
+ autoPlayBlocked: false,
+ volume: 1,
+ playbackRate: 1,
+ key: 0,
+ played:0,
+ loaded:0,
+ };
+
+ this.hideVolume = {
+ Vimeo: true,
+ Facebook: true,
+ ArcPlayer: true,
+ //YouTube: true,
+ };
+
+ this.opts = {
+ // default option for all players, can be overwritten
+ playerOptions: {
+ autoplay: true,
+ playsinline: true,
+ controls: isPresenter,
+ },
+ file: {
+ attributes: {
+ controls: isPresenter ? 'controls' : '',
+ autoplay: 'autoplay',
+ playsinline: 'playsinline',
+ },
+ },
+ facebook: {
+ controls: isPresenter,
+ },
+ dailymotion: {
+ params: {
+ controls: isPresenter,
+ },
+ },
+ youtube: {
+ playerVars: {
+ autoplay: 1,
+ modestbranding: 1,
+ autohide: 1,
+ rel: 0,
+ ecver: 2,
+ controls: isPresenter ? 1 : 0,
+ cc_lang_pref: document.getElementsByTagName('html')[0].lang.substring(0, 2),
+ },
+ },
+ peertube: {
+ isPresenter,
+ },
+ twitch: {
+ options: {
+ controls: isPresenter,
+ },
+ playerId: 'externalVideoPlayerTwitch',
+ },
+ preload: true,
+ showHoverToolBar: false,
+ };
+
+ this.registerVideoListeners = this.registerVideoListeners.bind(this);
+ this.autoPlayBlockDetected = this.autoPlayBlockDetected.bind(this);
+ this.handleFirstPlay = this.handleFirstPlay.bind(this);
+ this.handleReload = throttle({ interval: THROTTLE_RELOAD_INTERVAL }, this.handleReload.bind(this));
+ this.handleOnProgress = this.handleOnProgress.bind(this);
+ this.handleOnReady = this.handleOnReady.bind(this);
+ this.handleOnPlay = this.handleOnPlay.bind(this);
+ this.handleOnPause = this.handleOnPause.bind(this);
+ this.handleVolumeChanged = this.handleVolumeChanged.bind(this);
+ this.handleOnMuted = this.handleOnMuted.bind(this);
+ this.sendSyncMessage = this.sendSyncMessage.bind(this);
+ this.getCurrentPlaybackRate = this.getCurrentPlaybackRate.bind(this);
+ this.getCurrentTime = this.getCurrentTime.bind(this);
+ this.getCurrentVolume = this.getCurrentVolume.bind(this);
+ this.getMuted = this.getMuted.bind(this);
+ this.setPlaybackRate = this.setPlaybackRate.bind(this);
+ this.onBeforeUnload = this.onBeforeUnload.bind(this);
+ this.toggleSubtitle = this.toggleSubtitle.bind(this);
+
+ this.mobileHoverSetTimeout = null;
+ }
+
+ componentDidMount() {
+ const {
+ layoutContextDispatch,
+ } = this.props;
+
+ window.addEventListener('beforeunload', this.onBeforeUnload);
+
+ clearInterval(this.syncInterval);
+ clearTimeout(this.autoPlayTimeout);
+
+ VideoPlayer.clearVideoListeners();
+ this.registerVideoListeners();
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
+ value: true,
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_PRESENTATION_IS_OPEN,
+ value: true,
+ });
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const { isPresenter } = this.props;
+ const { playing } = this.state;
+
+ // If user is presenter we don't re-render playing state changes
+ // Because he's in control of the play/pause status
+ if (nextProps.isPresenter && isPresenter && nextState.playing !== playing) {
+ return false;
+ }
+
+ return true;
+ }
+
+ componentDidUpdate(prevProp) {
+ // Detect presenter change and redo the sync and listeners to reassign video to the new one
+ const { isPresenter } = this.props;
+ if (isPresenter !== prevProp.isPresenter) {
+ VideoPlayer.clearVideoListeners();
+
+ clearInterval(this.syncInterval);
+ clearTimeout(this.autoPlayTimeout);
+
+ this.registerVideoListeners();
+ }
+ }
+
+ componentWillUnmount() {
+ const {
+ layoutContextDispatch,
+ hidePresentationOnJoin,
+ } = this.props;
+
+ window.removeEventListener('beforeunload', this.onBeforeUnload);
+
+ VideoPlayer.clearVideoListeners();
+
+ clearInterval(this.syncInterval);
+ clearTimeout(this.autoPlayTimeout);
+
+ this.player = null;
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_HAS_EXTERNAL_VIDEO,
+ value: false,
+ });
+ layoutContextDispatch({
+ type: ACTIONS.SET_PRESENTATION_IS_OPEN,
+ value: Session.get('presentationLastState'),
+ });
+
+ if (hidePresentationOnJoin) {
+ layoutContextDispatch({
+ type: ACTIONS.SET_PRESENTATION_IS_OPEN,
+ value: false,
+ });
+ }
+ }
+
+ toggleSubtitle() {
+ this.setState((state) => {
+ return { subtitlesOn: !state.subtitlesOn };
+ }, () => {
+ const { subtitlesOn } = this.state;
+ const { isPresenter } = this.props;
+ const internalPlayer = this?.player?.getInternalPlayer();
+ if (!internalPlayer) return;
+ if (!isPresenter && subtitlesOn) {
+ if (typeof internalPlayer.setOption === 'function') {
+ internalPlayer.setOption('captions', 'reload', true);
+ }
+ } else if (typeof internalPlayer.unloadModule === 'function') {
+ internalPlayer.unloadModule('captions');
+ }
+ });
+ }
+
+ handleOnReady() {
+ const { hasPlayedBefore, playerIsReady } = this;
+
+ if (hasPlayedBefore || playerIsReady) {
+ return;
+ }
+
+ this.playerIsReady = true;
+
+ this.autoPlayTimeout = setTimeout(
+ this.autoPlayBlockDetected,
+ AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS * 1000,
+ );
+ }
+
+ handleFirstPlay() {
+ const { isPresenter } = this.props;
+ const { hasPlayedBefore } = this;
+
+ if (!hasPlayedBefore) {
+ this.hasPlayedBefore = true;
+ this.setState({ autoPlayBlocked: false });
+
+ clearTimeout(this.autoPlayTimeout);
+
+ if (isPresenter) {
+ this.sendSyncMessage('presenterReady');
+ }
+ }
+ }
+
+ handleOnPlay() {
+ const { isPresenter } = this.props;
+ const { playing } = this.state;
+
+ const curTime = this.getCurrentTime();
+
+ if (isPresenter && !playing) {
+ this.sendSyncMessage('play', { time: curTime });
+ }
+ this.setState({ playing: true });
+
+ this.handleFirstPlay();
+
+ if (!isPresenter && !playing) {
+ this.setState({ playing: false });
+ }
+ }
+
+ handleOnPause() {
+ const { isPresenter } = this.props;
+ const { playing } = this.state;
+
+ const curTime = this.getCurrentTime();
+
+ if (isPresenter && playing) {
+ this.sendSyncMessage('stop', { time: curTime });
+ }
+ this.setState({ playing: false });
+
+ this.handleFirstPlay();
+
+ if (!isPresenter && playing) {
+ this.setState({ playing: true });
+ }
+ }
+
+ handleOnProgress(data) {
+ const { mutedByEchoTest } = this.state;
+
+ const volume = this.getCurrentVolume();
+ const muted = this.getMuted();
+
+ const { played, loaded } = data;
+
+ this.setState({played, loaded});
+
+ if (!mutedByEchoTest) {
+ this.setState({ volume, muted });
+ }
+ }
+
+ handleVolumeChanged(volume) {
+ this.setState({ volume });
+ }
+
+ handleOnMuted(muted) {
+ const { mutedByEchoTest } = this.state;
+
+ if (!mutedByEchoTest) {
+ this.setState({ muted });
+ }
+ }
+
+ handleReload() {
+ const { key } = this.state;
+ // increment key and force a re-render of the video component
+ this.setState({ key: key + 1 });
+ }
+
+ onBeforeUnload() {
+ const { isPresenter } = this.props;
+
+ if (isPresenter) {
+ this.sendSyncMessage('stop');
+ }
+ }
+
+ static getDerivedStateFromProps(props) {
+ const { inEchoTest } = props;
+
+ return { mutedByEchoTest: inEchoTest };
+ }
+
+ getCurrentTime() {
+ if (this.player && this.player.getCurrentTime) {
+ return Math.round(this.player.getCurrentTime());
+ }
+ return 0;
+ }
+
+ getCurrentPlaybackRate() {
+ const intPlayer = this.player && this.player.getInternalPlayer();
+ const rate = (intPlayer && intPlayer.getPlaybackRate && intPlayer.getPlaybackRate());
+
+ return typeof (rate) === 'number' ? rate : 1;
+ }
+
+ setPlaybackRate(rate) {
+ const intPlayer = this.player && this.player.getInternalPlayer();
+ const currentRate = this.getCurrentPlaybackRate();
+
+ if (currentRate === rate) {
+ return;
+ }
+
+ this.setState({ playbackRate: rate });
+ if (intPlayer && intPlayer.setPlaybackRate) {
+ intPlayer.setPlaybackRate(rate);
+ }
+ }
+
+ getCurrentVolume() {
+ const { volume } = this.state;
+ const intPlayer = this.player && this.player.getInternalPlayer();
+
+ return (intPlayer && intPlayer.getVolume && intPlayer.getVolume() / 100.0) || volume;
+ }
+
+ getMuted() {
+ const { isPresenter } = this.props;
+ const { muted } = this.state;
+ const intPlayer = this?.player?.getInternalPlayer?.();
+
+ return isPresenter ? intPlayer?.isMuted?.() : muted;
+ }
+
+ autoPlayBlockDetected() {
+ this.setState({ autoPlayBlocked: true });
+ }
+
+ sendSyncMessage(msg, params) {
+ const timestamp = Date.now();
+
+ // If message is just a quick pause/un-pause just send nothing
+ const sinceLastMessage = (timestamp - this.lastMessageTimestamp) / 1000;
+ if ((
+ (msg === 'play' && this.lastMessage === 'stop')
+ || (msg === 'stop' && this.lastMessage === 'play'))
+ && sinceLastMessage < THROTTLE_INTERVAL_SECONDS) {
+ return clearTimeout(this.throttleTimeout);
+ }
+
+ // Ignore repeat presenter ready messages
+ if (this.lastMessage === msg && msg === 'presenterReady') {
+ logger.debug('Ignoring a repeated presenterReady message');
+ } else {
+ // Play/pause messages are sent with a delay, to permit cancelling it in case of
+ // quick sucessive play/pauses
+ const messageDelay = (msg === 'play' || msg === 'stop') ? THROTTLE_INTERVAL_SECONDS : 0;
+
+ this.throttleTimeout = setTimeout(() => {
+ sendMessage(msg, { ...params });
+ }, messageDelay * 1000);
+
+ this.lastMessage = msg;
+ this.lastMessageTimestamp = timestamp;
+ }
+ return true;
+ }
+
+ registerVideoListeners() {
+ const { isPresenter } = this.props;
+
+ if (isPresenter) {
+ this.syncInterval = setInterval(() => {
+ const { playing } = this.state;
+ const curTime = this.getCurrentTime();
+ const rate = this.getCurrentPlaybackRate();
+
+ // Always pause video if presenter is has not started sharing, e.g., blocked by autoplay
+ const playingState = this.hasPlayedBefore ? playing : false;
+
+ this.sendSyncMessage('playerUpdate', { rate, time: curTime, state: playingState });
+ }, SYNC_INTERVAL_SECONDS * 1000);
+ } else {
+ onMessage('play', ({ time }) => {
+ const { hasPlayedBefore, player } = this;
+
+ if (!player || !hasPlayedBefore) {
+ return;
+ }
+ this.seekTo(time);
+ this.setState({ playing: true });
+
+ logger.debug({ logCode: 'external_video_client_play' }, 'Play external video');
+ });
+
+ onMessage('stop', ({ time }) => {
+ const { hasPlayedBefore, player } = this;
+
+ if (!player || !hasPlayedBefore) {
+ return;
+ }
+ this.seekTo(time);
+ this.setState({ playing: false });
+
+ logger.debug({ logCode: 'external_video_client_stop' }, 'Stop external video');
+ });
+
+ onMessage('presenterReady', () => {
+ const { hasPlayedBefore } = this;
+
+ logger.debug({ logCode: 'external_video_presenter_ready' }, 'Presenter is ready to sync');
+
+ if (!hasPlayedBefore) {
+ this.setState({ playing: true });
+ }
+ });
+
+ onMessage('playerUpdate', (data) => {
+ const { hasPlayedBefore, player } = this;
+ const { playing } = this.state;
+ const { time, rate, state } = data;
+
+ if (!player || !hasPlayedBefore) {
+ return;
+ }
+
+ if (rate !== this.getCurrentPlaybackRate()) {
+ this.setPlaybackRate(rate);
+ logger.debug({
+ logCode: 'external_video_client_update_rate',
+ extraInfo: {
+ newRate: rate,
+ },
+ }, 'Change external video playback rate.');
+ }
+
+ this.seekTo(time);
+
+ const playingState = getPlayingState(state);
+ if (playing !== playingState) {
+ this.setState({ playing: playingState });
+ }
+ });
+ }
+ }
+
+ seekTo(time) {
+ const { player } = this;
+
+ if (!player) {
+ return logger.error('No player on seek');
+ }
+
+ // Seek if viewer has drifted too far away from presenter
+ if (Math.abs(this.getCurrentTime() - time) > SYNC_INTERVAL_SECONDS * 0.75) {
+ player.seekTo(time, true);
+ logger.debug({
+ logCode: 'external_video_client_update_seek',
+ extraInfo: { time },
+ }, `Seek external video to: ${time}`);
+ }
+ return true;
+ }
+
+ renderFullscreenButton() {
+ const { intl, fullscreenElementId, fullscreenContext } = this.props;
+
+ if (!ALLOW_FULLSCREEN) return null;
+
+ return (
+
+ );
+ }
+
+ render() {
+ const {
+ videoUrl,
+ isPresenter,
+ intl,
+ top,
+ left,
+ right,
+ height,
+ width,
+ fullscreenContext,
+ isResizing,
+ zIndex,
+ } = this.props;
+
+ const {
+ playing, playbackRate, mutedByEchoTest, autoPlayBlocked,
+ volume, muted, key, showHoverToolBar, played, loaded, subtitlesOn
+ } = this.state;
+
+ // This looks weird, but I need to get this nested player
+ const playerName = this.player && this.player.player
+ && this.player.player.player && this.player.player.player.constructor.name;
+
+ let toolbarStyle = 'hoverToolbar';
+
+ if (deviceInfo.isMobile && !showHoverToolBar) {
+ toolbarStyle = 'dontShowMobileHoverToolbar';
+ }
+
+ if (deviceInfo.isMobile && showHoverToolBar) {
+ toolbarStyle = 'showMobileHoverToolbar';
+ }
+ const isMinimized = width === 0 && height === 0;
+
+ return (
+
+ { this.playerParent = ref; }}
+ >
+ {
+ autoPlayBlocked
+ ? (
+
+ {intl.formatMessage(intlMessages.autoPlayWarning)}
+
+ )
+ : ''
+ }
+
+ { this.player = ref; }}
+ height="100%"
+ width="100%"
+ />
+ {
+ !isPresenter
+ ? [
+ (
+
+
+
+
+ {playerName === 'YouTube' && (
+
+ )}
+
+ {this.renderFullscreenButton()}
+
+
+
+
+
+
+
+ ),
+ (deviceInfo.isMobile && playing) && (
+ { this.overlay = ref; }}
+ onTouchStart={() => {
+ clearTimeout(this.mobileHoverSetTimeout);
+ this.setState({ showHoverToolBar: true });
+ }}
+ onTouchEnd={() => {
+ this.mobileHoverSetTimeout = setTimeout(
+ () => this.setState({ showHoverToolBar: false }),
+ 5000,
+ );
+ }}
+ />
+ ),
+ ]
+ : null
+ }
+
+
+ );
+ }
+}
+
+VideoPlayer.propTypes = {
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func.isRequired,
+ }).isRequired,
+ fullscreenElementId: PropTypes.string.isRequired,
+ fullscreenContext: PropTypes.bool.isRequired,
+};
+
+export default injectIntl(injectWbResizeEvent(VideoPlayer));
diff --git a/src/2.7.12/imports/ui/components/external-video-player/container.jsx b/src/2.7.12/imports/ui/components/external-video-player/container.jsx
new file mode 100644
index 00000000..a97d6f0d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/container.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import { Session } from 'meteor/session';
+import { getVideoUrl } from './service';
+import ExternalVideoComponent from './component';
+import {
+ layoutSelect,
+ layoutSelectInput,
+ layoutSelectOutput,
+ layoutDispatch,
+} from '../layout/context';
+
+const ExternalVideoContainer = (props) => {
+ const fullscreenElementId = 'ExternalVideo';
+ const externalVideo = layoutSelectOutput((i) => i.externalVideo);
+ const cameraDock = layoutSelectInput((i) => i.cameraDock);
+ const { isResizing } = cameraDock;
+ const layoutContextDispatch = layoutDispatch();
+ const fullscreen = layoutSelect((i) => i.fullscreen);
+ const { element } = fullscreen;
+ const fullscreenContext = (element === fullscreenElementId);
+
+ return (
+
+ );
+};
+
+const LAYOUT_CONFIG = Meteor.settings.public.layout;
+
+export default withTracker(({ isPresenter }) => {
+ const inEchoTest = Session.get('inEchoTest');
+ return {
+ inEchoTest,
+ isPresenter,
+ videoUrl: getVideoUrl(),
+ };
+})(ExternalVideoContainer);
diff --git a/src/2.7.12/imports/ui/components/external-video-player/custom-players/arc-player.jsx b/src/2.7.12/imports/ui/components/external-video-player/custom-players/arc-player.jsx
new file mode 100644
index 00000000..e87a24da
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/custom-players/arc-player.jsx
@@ -0,0 +1,208 @@
+import loadScript from 'load-script';
+import React, { Component } from 'react'
+
+const MATCH_URL = new RegExp("https?:\/\/(.*)(instructuremedia.com)(\/embed)?\/([-abcdef0-9]+)");
+
+const SDK_URL = 'https://files.instructuremedia.com/instructure-media-script/instructure-media-1.1.0.js';
+
+const EMBED_PATH = "/embed/";
+
+// Util function to load an external SDK or return the SDK if it is already loaded
+// From https://github.com/CookPete/react-player/blob/master/src/utils.js
+const resolves = {};
+export function getSDK (url, sdkGlobal, sdkReady = null, isLoaded = () => true, fetchScript = loadScript) {
+ if (window[sdkGlobal] && isLoaded(window[sdkGlobal])) {
+ return Promise.resolve(window[sdkGlobal])
+ }
+ return new Promise((resolve, reject) => {
+ // If we are already loading the SDK, add the resolve
+ // function to the existing array of resolve functions
+ if (resolves[url]) {
+ resolves[url].push(resolve);
+ return
+ }
+ resolves[url] = [resolve];
+ const onLoaded = sdk => {
+ // When loaded, resolve all pending promises
+ resolves[url].forEach(resolve => resolve(sdk))
+ };
+ if (sdkReady) {
+ const previousOnReady = window[sdkReady];
+ window[sdkReady] = function () {
+ if (previousOnReady) previousOnReady();
+ onLoaded(window[sdkGlobal])
+ }
+ }
+ fetchScript(url, err => {
+ if (err) {
+ reject(err);
+ }
+ window[sdkGlobal] = url;
+ if (!sdkReady) {
+ onLoaded(window[sdkGlobal])
+ }
+ })
+ })
+}
+
+export class ArcPlayer extends Component {
+ static displayName = 'ArcPlayer';
+
+ static canPlay = url => {
+ return MATCH_URL.test(url)
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.currentTime = 0;
+ this.updateCurrentTime = this.updateCurrentTime.bind(this);
+ this.getCurrentTime = this.getCurrentTime.bind(this);
+ this.getEmbedUrl = this.getEmbedUrl.bind(this);
+ this.onStateChange = this.onStateChange.bind(this);
+ }
+
+ componentDidMount () {
+ this.props.onMount && this.props.onMount(this)
+ }
+
+ load() {
+ new Promise((resolve, reject) => {
+ this.render();
+ resolve();
+ })
+ .then(() => { return getSDK(SDK_URL, 'ArcPlayer') })
+ .then(() => {
+ this.player = new InstructureMedia.Player('arcPlayerContainer', {
+ height: '100%',
+ width: '100%',
+ embedUrl: this.getEmbedUrl(),
+ events: {
+ onStateChange: this.onStateChange,
+ }
+ });
+ this.player.playVideo();
+ });
+ }
+
+ onStateChange(event) {
+ if (!this.player) {
+ return;
+ }
+
+ this.player.getCurrentTime().then((t) => {
+ this.updateCurrentTime(t.data);
+ });
+
+ if (event.data === "CUED") {
+ this.props.onReady();
+ } else if (event.data === "PLAYING") {
+ this.props.onPlay && this.props.onPlay();
+ } else if (event.data === "PAUSED") {
+ this.props.onPause && this.props.onPause();
+ } else if (event.data === "SEEKED") {
+ // TODO
+ } else if (event.data === "SEEKING") {
+ // TODO
+ }
+ }
+
+ updateCurrentTime(e) {
+ this.currentTime = e;
+ }
+
+ getVideoId() {
+ const { url } = this.props;
+ const m = url.match(MATCH_URL);
+ return m && m[4];
+ }
+
+ getHostUrl() {
+ const { url } = this.props;
+ const m = url.match(MATCH_URL);
+ return m && 'https://' + m[1] + m[2];
+ }
+
+ getEmbedUrl() {
+ let url = this.getHostUrl() + EMBED_PATH + this.getVideoId();
+ return url;
+ }
+
+ play() {
+ if (this.player) {
+ this.player.playVideo();
+ }
+ }
+
+ pause() {
+ if (this.player) {
+ this.player.pauseVideo();
+ }
+ }
+
+ stop() {
+ // TODO: STOP
+ }
+
+ seekTo(seconds) {
+ if (this.player) {
+ this.player.seekTo(seconds);
+ }
+ }
+
+ setVolume(fraction) {
+ // console.log("SET VOLUME");
+ }
+
+ setLoop(loop) {
+ // console.log("SET LOOP");
+ }
+
+ mute() {
+ // console.log("SET MUTE");
+ }
+
+ unmute() {
+ // console.log("SET UNMUTE");
+ }
+
+ getDuration() {
+ //console.log("GET DURATION");
+ }
+
+ getCurrentTime () {
+ if (this.player) {
+ this.player.getCurrentTime().then((t) => {
+ this.updateCurrentTime(t.data);
+ });
+ }
+
+ return this.currentTime;
+ }
+
+ getSecondsLoaded () {
+ }
+
+ render () {
+ const style = {
+ width: '100%',
+ height: '100%',
+ overflow: 'hidden',
+ backgroundColor: 'black'
+ };
+ return (
+ {
+ this.container = container;
+ }}
+ >
+
+ )
+ }
+}
+
+export default ArcPlayer;
+
diff --git a/src/2.7.12/imports/ui/components/external-video-player/custom-players/panopto.jsx b/src/2.7.12/imports/ui/components/external-video-player/custom-players/panopto.jsx
new file mode 100644
index 00000000..d7a88663
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/custom-players/panopto.jsx
@@ -0,0 +1,16 @@
+const MATCH_URL = /https?\:\/\/([^\/]+\/Panopto)(\/Pages\/Viewer\.aspx\?id=)([-a-zA-Z0-9]+)/;
+
+export class Panopto {
+
+ static canPlay = url => {
+ return MATCH_URL.test(url)
+ };
+
+ static getSocialUrl(url) {
+ const m = url.match(MATCH_URL);
+ return 'https://' + m[1] + '/Podcast/Social/' + m[3] + '.mp4';
+ }
+}
+
+export default Panopto;
+
diff --git a/src/2.7.12/imports/ui/components/external-video-player/custom-players/peertube.jsx b/src/2.7.12/imports/ui/components/external-video-player/custom-players/peertube.jsx
new file mode 100644
index 00000000..13f5cb3a
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/custom-players/peertube.jsx
@@ -0,0 +1,216 @@
+import loadScript from 'load-script';
+import React, { Component } from 'react'
+
+//To work with PeerTube >=v3.3 URL patterns
+const MATCH_URL = new RegExp("(https?)://(.*)(/videos/watch/|/w/)(.*)");
+
+const SDK_URL = 'https://unpkg.com/@peertube/embed-api@0.0.4/build/player.min.js';
+
+// Util function to load an external SDK or return the SDK if it is already loaded
+// From https://github.com/CookPete/react-player/blob/master/src/utils.js
+const resolves = {};
+export function getSDK (url, sdkGlobal, sdkReady = null, isLoaded = () => true, fetchScript = loadScript) {
+ if (window[sdkGlobal] && isLoaded(window[sdkGlobal])) {
+ return Promise.resolve(window[sdkGlobal])
+ }
+ return new Promise((resolve, reject) => {
+ // If we are already loading the SDK, add the resolve
+ // function to the existing array of resolve functions
+ if (resolves[url]) {
+ resolves[url].push(resolve);
+ return
+ }
+ resolves[url] = [resolve];
+ const onLoaded = sdk => {
+ // When loaded, resolve all pending promises
+ resolves[url].forEach(resolve => resolve(sdk))
+ };
+ if (sdkReady) {
+ const previousOnReady = window[sdkReady];
+ window[sdkReady] = function () {
+ if (previousOnReady) previousOnReady();
+ onLoaded(window[sdkGlobal])
+ }
+ }
+ fetchScript(url, err => {
+ if (err) {
+ reject(err);
+ }
+ window[sdkGlobal] = url;
+ if (!sdkReady) {
+ onLoaded(window[sdkGlobal])
+ }
+ })
+ })
+}
+
+export class PeerTubePlayer extends Component {
+ static displayName = 'PeerTubePlayer';
+
+ static canPlay = url => {
+ return MATCH_URL.test(url)
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.player = this;
+ this._player = null;
+
+ this.currentTime = 0;
+ this.playbackRate = 1;
+ this.getCurrentTime = this.getCurrentTime.bind(this);
+ this.getEmbedUrl = this.getEmbedUrl.bind(this);
+ this.setupEvents = this.setupEvents.bind(this);
+ }
+
+ componentDidMount () {
+ this.props.onMount && this.props.onMount(this)
+ }
+
+ getEmbedUrl = () => {
+ const { url, config } = this.props;
+ const m = MATCH_URL.exec(url);
+
+ const isPresenter = config && config.peertube && config.peertube.isPresenter;
+
+ return `${m[1]}://${m[2]}/videos/embed/${m[4]}?api=1&controls=${isPresenter}`;
+ };
+
+ load() {
+ new Promise((resolve, reject) => {
+ this.render();
+ resolve();
+ })
+ .then(() => { return getSDK(SDK_URL, 'PeerTube') })
+ .then(() => {
+ this._player = new window['PeerTubePlayer'](this.container);
+
+ this.setupEvents();
+
+ return this._player.ready.then(() => {
+ return this.props.onReady();
+ });
+ });
+ }
+
+ setupEvents(event) {
+ const player = this._player;
+
+ if (!player) {
+ return;
+ }
+
+ player.addEventListener("playbackStatusUpdate", (data) => {
+ this.currentTime = data.position;
+ });
+ player.addEventListener("playbackStatusChange", (data) => {
+ if (data === 'playing') {
+ this.props.onPlay();
+ } else {
+ this.props.onPause();
+ }
+ });
+
+ }
+
+ play() {
+ if (this._player) {
+ this._player.play();
+ }
+ }
+
+ pause() {
+ if (this._player) {
+ this._player.pause();
+ }
+ }
+
+ stop() {
+ }
+
+ seekTo(seconds) {
+ if (this._player) {
+ this._player.seek(seconds);
+ }
+ }
+
+ setVolume(fraction) {
+ this._player.setVolume(fraction);
+ }
+
+ getVolume() {
+ return this._player.getVolume();
+ }
+
+ setLoop(loop) {
+ // console.log("SET LOOP");
+ }
+
+ mute() {
+ // console.log("SET MUTE");
+ }
+
+ unmute() {
+ // console.log("SET UNMUTE");
+ }
+
+ getDuration() {
+ //console.log("GET DURATION");
+ }
+
+ getCurrentTime () {
+ return this.currentTime;
+ }
+
+ getSecondsLoaded () {
+ }
+
+ getPlaybackRate () {
+
+ if (this._player) {
+ this._player.getPlaybackRate().then((rate) => {
+ this.playbackRate = rate;
+ });
+ }
+
+ return this.playbackRate;
+ }
+
+ setPlaybackRate (rate) {
+
+ if (this._player) {
+ this._player.setPlaybackRate(rate);
+ }
+ }
+
+ render () {
+ const style = {
+ width: '100%',
+ height: '100%',
+ margin: 0,
+ padding: 0,
+ border: 0,
+ overflow: 'hidden',
+ };
+ const { url } = this.props;
+
+ return (
+
+ )
+ }
+}
+
+export default PeerTubePlayer;
+
diff --git a/src/2.7.12/imports/ui/components/external-video-player/modal/component.jsx b/src/2.7.12/imports/ui/components/external-video-player/modal/component.jsx
new file mode 100644
index 00000000..4601fd54
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/modal/component.jsx
@@ -0,0 +1,147 @@
+import React, { Component } from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import { isUrlValid } from '../service';
+import Settings from '/imports/ui/services/settings';
+import Styled from './styles';
+
+const intlMessages = defineMessages({
+ start: {
+ id: 'app.externalVideo.start',
+ description: 'Share external video',
+ },
+ urlError: {
+ id: 'app.externalVideo.urlError',
+ description: 'Not a video URL error',
+ },
+ input: {
+ id: 'app.externalVideo.input',
+ description: 'Video URL',
+ },
+ urlInput: {
+ id: 'app.externalVideo.urlInput',
+ description: 'URL input field placeholder',
+ },
+ title: {
+ id: 'app.externalVideo.title',
+ description: 'Modal title',
+ },
+ close: {
+ id: 'app.externalVideo.close',
+ description: 'Close',
+ },
+ note: {
+ id: 'app.externalVideo.noteLabel',
+ description: 'provides hint about Shared External videos',
+ },
+});
+
+class ExternalVideoModal extends Component {
+ constructor(props) {
+ super(props);
+
+ const { videoUrl } = props;
+
+ this.state = {
+ url: videoUrl,
+ sharing: videoUrl,
+ };
+
+ this.startWatchingHandler = this.startWatchingHandler.bind(this);
+ this.updateVideoUrlHandler = this.updateVideoUrlHandler.bind(this);
+ this.renderUrlError = this.renderUrlError.bind(this);
+ this.updateVideoUrlHandler = this.updateVideoUrlHandler.bind(this);
+ }
+
+ startWatchingHandler() {
+ const {
+ startWatching,
+ setIsOpen,
+ } = this.props;
+
+ const { url } = this.state;
+
+ startWatching(url.trim());
+ setIsOpen(false);
+ }
+
+ updateVideoUrlHandler(ev) {
+ this.setState({ url: ev.target.value });
+ }
+
+ renderUrlError() {
+ const { intl } = this.props;
+ const { url } = this.state;
+ const { animations } = Settings.application;
+
+ const valid = (!url || url.length <= 3) || isUrlValid(url);
+
+ return (
+ !valid
+ ? (
+
+ {intl.formatMessage(intlMessages.urlError)}
+
+ )
+ : null
+ );
+ }
+
+ render() {
+ const { intl, setIsOpen, isOpen, onRequestClose, priority, } = this.props;
+ const { url, sharing } = this.state;
+ const { animations } = Settings.application;
+
+ const startDisabled = !isUrlValid(url);
+
+ return (
+ setIsOpen(false)}
+ contentLabel={intl.formatMessage(intlMessages.title)}
+ title={intl.formatMessage(intlMessages.title)}
+ {...{
+ setIsOpen,
+ isOpen,
+ onRequestClose,
+ priority,
+ }}
+ >
+
+
+
+ {intl.formatMessage(intlMessages.input)}
+ { e.stopPropagation(); }}
+ onCut={(e) => { e.stopPropagation(); }}
+ onCopy={(e) => { e.stopPropagation(); }}
+ />
+
+
+ {intl.formatMessage(intlMessages.note)}
+
+
+
+
+ {this.renderUrlError()}
+
+
+
+
+
+ );
+ }
+}
+
+export default injectIntl(ExternalVideoModal);
diff --git a/src/2.7.12/imports/ui/components/external-video-player/modal/container.jsx b/src/2.7.12/imports/ui/components/external-video-player/modal/container.jsx
new file mode 100644
index 00000000..9cff24d5
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/modal/container.jsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import ExternalVideoModal from './component';
+import { startWatching, getVideoUrl } from '../service';
+
+const ExternalVideoModalContainer = props => ;
+
+export default withTracker(() => ({
+ startWatching,
+ videoUrl: getVideoUrl(),
+}))(ExternalVideoModalContainer);
diff --git a/src/2.7.12/imports/ui/components/external-video-player/modal/styles.js b/src/2.7.12/imports/ui/components/external-video-player/modal/styles.js
new file mode 100644
index 00000000..872ecf35
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/modal/styles.js
@@ -0,0 +1,110 @@
+import styled from 'styled-components';
+import {
+ borderSize,
+ borderRadius,
+ smPaddingY,
+ mdPaddingX,
+} from '/imports/ui/stylesheets/styled-components/general';
+import {
+ colorText,
+ colorGrayLighter,
+ colorGray,
+ colorBlueLight,
+ colorPrimary,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import { fontSizeSmall } from '/imports/ui/stylesheets/styled-components/typography';
+import ModalSimple from '/imports/ui/components/common/modal/simple/component';
+import Button from '/imports/ui/components/common/button/component';
+
+const UrlError = styled.div`
+ color: red;
+ padding: 1em 0 2.5em 0;
+
+ ${({ animations }) => animations && `
+ transition: 1s;
+ `}
+`;
+
+const ExternalVideoModal = styled(ModalSimple)`
+ padding: 1rem;
+ min-height: 23rem;
+`;
+
+const Content = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 0;
+ margin-right: auto;
+ margin-left: auto;
+ width: 100%;
+`;
+
+const VideoUrl = styled.div`
+ margin: 0 ${borderSize} 0 ${borderSize};
+
+ & > label {
+ display: block;
+ }
+
+ & > label input {
+ display: block;
+ margin: 10px 0 10px 0;
+ padding: 0.4em;
+ color: ${colorText};
+ line-height: 2rem;
+ width: 100%;
+ font-family: inherit;
+ font-weight: inherit;
+ border: 1px solid ${colorGrayLighter};
+ border-radius: ${borderRadius};
+
+ ${({ animations }) => animations && `
+ transition: box-shadow .2s;
+ `}
+
+ &:focus {
+ outline: none;
+ border-radius: ${borderSize};
+ box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
+ }
+ }
+
+ & > span {
+ font-weight: 600;
+ }
+`;
+
+const ExternalVideoNote = styled.div`
+ color: ${colorGray};
+ font-size: ${fontSizeSmall};
+ font-style: italic;
+ padding-top: ${smPaddingY};
+`;
+
+const StartButton = styled(Button)`
+ display: flex;
+ align-self: center;
+
+ &:focus {
+ outline: none !important;
+ }
+
+ & > i {
+ color: #3c5764;
+ }
+
+ margin: 0;
+ display: block;
+ position: absolute;
+ bottom: ${mdPaddingX};
+`;
+
+export default {
+ UrlError,
+ ExternalVideoModal,
+ Content,
+ VideoUrl,
+ ExternalVideoNote,
+ StartButton,
+};
diff --git a/src/2.7.12/imports/ui/components/external-video-player/service.js b/src/2.7.12/imports/ui/components/external-video-player/service.js
new file mode 100644
index 00000000..dd758449
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/service.js
@@ -0,0 +1,103 @@
+import { ExternalVideoMeetings } from '/imports/api/meetings';
+import Auth from '/imports/ui/services/auth';
+
+import { getStreamer } from '/imports/api/external-videos';
+import { makeCall } from '/imports/ui/services/api';
+import NotesService from '/imports/ui/components/notes/service';
+
+import ReactPlayer from 'react-player';
+
+import Panopto from './custom-players/panopto';
+
+const YOUTUBE_SHORTS_REGEX = new RegExp(/^(?:https?:\/\/)?(?:www\.)?(youtube\.com\/shorts)\/.+$/);
+
+const isUrlValid = (url) => {
+ if (YOUTUBE_SHORTS_REGEX.test(url)) {
+ const shortsUrl = url.replace('shorts/', 'watch?v=');
+
+ return /^https.*$/.test(shortsUrl) && (ReactPlayer.canPlay(shortsUrl) || Panopto.canPlay(shortsUrl));
+ }
+
+ return /^https.*$/.test(url) && (ReactPlayer.canPlay(url) || Panopto.canPlay(url));
+};
+
+const startWatching = (url) => {
+ let externalVideoUrl = url;
+
+ if (YOUTUBE_SHORTS_REGEX.test(url)) {
+ const shortsUrl = url.replace('shorts/', 'watch?v=');
+ externalVideoUrl = shortsUrl;
+ } else if (Panopto.canPlay(url)) {
+ externalVideoUrl = Panopto.getSocialUrl(url);
+ }
+
+ // Close Shared Notes if open.
+ NotesService.pinSharedNotes(false);
+
+ makeCall('startWatchingExternalVideo', externalVideoUrl);
+};
+
+const stopWatching = () => {
+ makeCall('stopWatchingExternalVideo');
+};
+
+let lastMessage = null;
+
+const sendMessage = (event, data) => {
+
+ // don't re-send repeated update messages
+ if (lastMessage && lastMessage.event === event
+ && event === 'playerUpdate' && lastMessage.time === data.time) {
+ return;
+ }
+
+ // don't register to redis a viewer joined message
+ if (event === 'viewerJoined') {
+ return;
+ }
+
+ lastMessage = { ...data, event };
+
+ // Use an integer for playing state
+ // 0: stopped 1: playing
+ // We might use more states in the future
+ data.state = data.state ? 1 : 0;
+
+ makeCall('emitExternalVideoEvent', { status: event, playerStatus: data });
+};
+
+const onMessage = (message, func) => {
+ const streamer = getStreamer(Auth.meetingID);
+ streamer.on(message, func);
+};
+
+const removeAllListeners = (eventType) => {
+ const streamer = getStreamer(Auth.meetingID);
+ streamer.removeAllListeners(eventType);
+};
+
+const getVideoUrl = () => {
+ const meetingId = Auth.meetingID;
+ const externalVideo = ExternalVideoMeetings
+ .findOne({ meetingId }, { fields: { externalVideoUrl: 1 } });
+
+ return externalVideo && externalVideo.externalVideoUrl;
+};
+
+// Convert state (Number) to playing (Boolean)
+const getPlayingState = (state) => {
+ if (state === 1) return true;
+
+ return false;
+};
+
+export {
+ sendMessage,
+ onMessage,
+ removeAllListeners,
+ getVideoUrl,
+ isUrlValid,
+ startWatching,
+ stopWatching,
+ getPlayingState,
+};
diff --git a/src/2.7.12/imports/ui/components/external-video-player/styles.js b/src/2.7.12/imports/ui/components/external-video-player/styles.js
new file mode 100644
index 00000000..fd056d09
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/styles.js
@@ -0,0 +1,119 @@
+import styled from 'styled-components';
+import ReactPlayer from 'react-player';
+
+const VideoPlayerWrapper = styled.div`
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ ${({ fullscreen }) => fullscreen && `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ z-index: 99;
+ `}
+`;
+
+const AutoPlayWarning = styled.p`
+ position: absolute;
+ z-index: 100;
+ font-size: x-large;
+ color: white;
+ width: 100%;
+ background-color: rgba(6,23,42,0.5);
+ bottom: 20%;
+ vertical-align: middle;
+ text-align: center;
+ pointer-events: none;
+`;
+
+const VideoPlayer = styled(ReactPlayer)`
+ width: 100%;
+ height: 100%;
+ & > iframe {
+ display: flex;
+ flex-flow: column;
+ flex-grow: 1;
+ flex-shrink: 1;
+ position: relative;
+ overflow-x: hidden;
+ overflow-y: auto;
+ border-style: none;
+ border-bottom: none;
+ }
+`;
+
+const MobileControlsOverlay = styled.span`
+ position: absolute;
+ top:0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: transparent;
+`;
+
+const HoverToolbar = styled.div`
+ ${({ toolbarStyle }) => toolbarStyle === 'hoverToolbar' && `
+ display: none;
+
+ :hover > & {
+ display: flex;
+ }
+ `}
+
+ ${({ toolbarStyle }) => toolbarStyle === 'dontShowMobileHoverToolbar' && `
+ display: none;
+ `}
+
+ ${({ toolbarStyle }) => toolbarStyle === 'showMobileHoverToolbar' && `
+ display: flex;
+ z-index: 2;
+ `}
+`;
+
+const ProgressBar = styled.div`
+ position: absolute;
+ bottom: 0;
+ height: 5px;
+ width: 100%;
+
+ background-color: transparent;
+`;
+
+const Loaded = styled.div`
+ height: 100%;
+ background-color: gray;
+`;
+
+const Played = styled.div`
+ height: 100%;
+ background-color: #DF2721;
+`;
+
+const ButtonsWrapper = styled.div`
+ position: absolute;
+ right: auto;
+ left: 0;
+ bottom: 0;
+ top: 0;
+ display: flex;
+
+ [dir="rtl"] & {
+ right: 0;
+ left : auto;
+ }
+`;
+
+export default {
+ VideoPlayerWrapper,
+ AutoPlayWarning,
+ VideoPlayer,
+ MobileControlsOverlay,
+ HoverToolbar,
+ ProgressBar,
+ Loaded,
+ Played,
+ ButtonsWrapper,
+};
diff --git a/src/2.7.12/imports/ui/components/external-video-player/subtitles/component.jsx b/src/2.7.12/imports/ui/components/external-video-player/subtitles/component.jsx
new file mode 100644
index 00000000..e61329fa
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/subtitles/component.jsx
@@ -0,0 +1,25 @@
+import React, { Component } from 'react';
+import Styled from './styles';
+
+class Subtitles extends Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const { toggleSubtitle, label } = this.props;
+ return (
+
+ toggleSubtitle()}
+ label={label}
+ hideLabel
+ />
+
+ );
+ }
+}
+
+export default Subtitles;
diff --git a/src/2.7.12/imports/ui/components/external-video-player/subtitles/styles.js b/src/2.7.12/imports/ui/components/external-video-player/subtitles/styles.js
new file mode 100644
index 00000000..e9f491f1
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/subtitles/styles.js
@@ -0,0 +1,46 @@
+import styled from 'styled-components';
+import Button from '/imports/ui/components/common/button/component';
+
+import { colorTransparent } from '/imports/ui/stylesheets/styled-components/palette';
+
+const SubtitlesWrapper = styled.div`
+ background-color: ${colorTransparent};
+ cursor: pointer;
+ border: 0;
+ z-index: 2;
+ margin: 2px;
+
+ [dir="rtl"] & {
+ right: 0;
+ left : auto;
+ }
+`;
+
+const SubtitlesButton = styled(Button)`
+ &,
+ &:active,
+ &:hover,
+ &:focus {
+ border: none !important;
+
+ i {
+ border: none !important;
+ background-color: ${colorTransparent} !important;
+ }
+ }
+
+ padding: 5px;
+
+ &:hover {
+ border: 0;
+ }
+
+ i {
+ font-size: 1rem;
+ }
+`;
+
+export default {
+ SubtitlesWrapper,
+ SubtitlesButton,
+};
diff --git a/src/2.7.12/imports/ui/components/external-video-player/volume-slider/component.jsx b/src/2.7.12/imports/ui/components/external-video-player/volume-slider/component.jsx
new file mode 100644
index 00000000..9c5e874c
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/volume-slider/component.jsx
@@ -0,0 +1,91 @@
+import React, { Component } from 'react';
+import Styled from './styles';
+
+class VolumeSlider extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ volume: props.volume,
+ muted: props.muted,
+ };
+
+ this.handleOnChange = this.handleOnChange.bind(this);
+ this.getVolumeIcon = this.getVolumeIcon.bind(this);
+ this.setMuted = this.setMuted.bind(this);
+ }
+
+ componentDidUpdate(prevProp) {
+ const { volume, muted } = this.props;
+ const { volume: prevVolume, muted: prevMuted } = prevProp;
+
+ if (prevVolume !== volume) {
+ this.handleOnChange(volume);
+ }
+
+ if (prevMuted !== muted) {
+ this.setMuted(muted);
+ }
+ }
+
+ handleOnChange(volume) {
+ const { onVolumeChanged } = this.props;
+ onVolumeChanged(volume);
+
+ this.setState({ volume }, () => {
+ const { volume: stateVolume, muted } = this.state;
+ if (muted && stateVolume > 0) { // unmute if volume was raised during mute
+ this.setMuted(false);
+ } else if (stateVolume <= 0) { // mute if volume is turned to 0
+ this.setMuted(true);
+ }
+ });
+ }
+
+ setMuted(muted) {
+ const { onMuted } = this.props;
+ this.setState({ muted }, () => onMuted(muted));
+ }
+
+ getVolumeIcon() {
+ const { muted, volume } = this.state;
+
+ if (muted || volume <= 0) return 'volume_off';
+
+ if (volume <= 0.25) return 'volume_mute';
+
+ if (volume <= 0.75) return 'volume_down';
+
+ return 'volume_up';
+ }
+
+ render() {
+ const { muted, volume } = this.state;
+ const { hideVolume } = this.props;
+
+ if (hideVolume) {
+ return null;
+ }
+
+ return (
+
+ this.setMuted(!muted)}>
+
+
+ this.handleOnChange(e.target.valueAsNumber)}
+ />
+
+ );
+ }
+}
+
+export default VolumeSlider;
diff --git a/src/2.7.12/imports/ui/components/external-video-player/volume-slider/styles.js b/src/2.7.12/imports/ui/components/external-video-player/volume-slider/styles.js
new file mode 100644
index 00000000..0a70e8fd
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/external-video-player/volume-slider/styles.js
@@ -0,0 +1,37 @@
+import styled from 'styled-components';
+
+const Slider = styled.div`
+ width: 0.9em;
+ display: flex;
+ position: absolute;
+ bottom: 2.5em;
+ left: 1em;
+ padding: 0.25rem 0.5rem;
+ min-width: 200px;
+ background-color: rgba(0,0,0,0.5);
+ border-radius: 32px;
+
+ i {
+ color: white;
+ transition: 0.5s;
+ font-size: 200%;
+ cursor: pointer;
+ }
+ z-index: 2;
+`;
+
+const Volume = styled.span`
+ margin-right: 0.5em;
+ cursor: pointer;
+`;
+
+const VolumeSlider = styled.input`
+ width: 100%;
+ cursor: pointer;
+`;
+
+export default {
+ Slider,
+ Volume,
+ VolumeSlider,
+};
diff --git a/src/2.7.12/imports/ui/components/join-handler/component.jsx b/src/2.7.12/imports/ui/components/join-handler/component.jsx
new file mode 100644
index 00000000..6345e6da
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/join-handler/component.jsx
@@ -0,0 +1,267 @@
+import React, { Component } from 'react';
+import { Session } from 'meteor/session';
+import PropTypes from 'prop-types';
+import SanitizeHTML from 'sanitize-html';
+import Auth from '/imports/ui/services/auth';
+import { setCustomLogoUrl, setModeratorOnlyMessage } from '/imports/ui/components/user-list/service';
+import { makeCall } from '/imports/ui/services/api';
+import logger from '/imports/startup/client/logger';
+import LoadingScreen from '/imports/ui/components/common/loading-screen/component';
+import { CurrentUser } from '/imports/api/users';
+import Settings from '/imports/ui/services/settings';
+import { updateSettings } from '/imports/ui/components/settings/service';
+import { LAYOUT_TYPE } from '/imports/ui/components/layout/enums';
+
+const propTypes = {
+ children: PropTypes.element.isRequired,
+};
+
+class JoinHandler extends Component {
+ static setError(codeError) {
+ if (codeError) Session.set('codeError', codeError);
+ }
+
+ constructor(props) {
+ super(props);
+ this.fetchToken = this.fetchToken.bind(this);
+
+ this.state = {
+ joined: false,
+ hasAlreadyJoined: false,
+ };
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
+
+ if (!this.firstJoinTime) {
+ this.firstJoinTime = new Date();
+ }
+ Tracker.autorun((c) => {
+ const {
+ connected,
+ status,
+ } = Meteor.status();
+ const { hasAlreadyJoined } = this.state;
+ if (status === 'connecting' && !hasAlreadyJoined) {
+ this.setState({ joined: false });
+ }
+
+ logger.debug(`Initial connection status change. status: ${status}, connected: ${connected}`);
+ if (connected) {
+ const msToConnect = (new Date() - this.firstJoinTime) / 1000;
+ const secondsToConnect = parseFloat(msToConnect).toFixed(2);
+
+ logger.info({
+ logCode: 'joinhandler_component_initial_connection_time',
+ extraInfo: {
+ attemptForUserInfo: Auth.fullInfo,
+ timeToConnect: secondsToConnect,
+ },
+ }, `Connection to Meteor took ${secondsToConnect}s`);
+
+ this.firstJoinTime = undefined;
+ this.fetchToken();
+ } else if (status === 'failed') {
+ c.stop();
+
+ const msToConnect = (new Date() - this.firstJoinTime) / 1000;
+ const secondsToConnect = parseFloat(msToConnect).toFixed(2);
+ logger.info({
+ logCode: 'joinhandler_component_initial_connection_failed',
+ extraInfo: {
+ attemptForUserInfo: Auth.fullInfo,
+ timeToConnect: secondsToConnect,
+ },
+ }, `Connection to Meteor failed, took ${secondsToConnect}s`);
+
+ JoinHandler.setError('400');
+ Session.set('errorMessageDescription', 'Failed to connect to server');
+ this.firstJoinTime = undefined;
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false;
+ }
+
+ async fetchToken() {
+ const { hasAlreadyJoined } = this.state;
+ const APP = Meteor.settings.public.app;
+ if (!this._isMounted) return;
+
+ const urlParams = new URLSearchParams(window.location.search);
+ const sessionToken = urlParams.get('sessionToken');
+
+ if (!sessionToken) {
+ JoinHandler.setError('400');
+ Session.set('errorMessageDescription', 'Session token was not provided');
+ }
+
+ // Old credentials stored in memory were being used when joining a new meeting
+ if (!hasAlreadyJoined) {
+ Auth.clearCredentials();
+ }
+ const logUserInfo = () => {
+ const userInfo = window.navigator;
+
+ // Browser information is sent once on startup
+ // Sent here instead of Meteor.startup, as the
+ // user might not be validated by then, thus user's data
+ // would not be sent with this information
+ const clientInfo = {
+ language: userInfo.language,
+ userAgent: userInfo.userAgent,
+ screenSize: { width: window.screen.width, height: window.screen.height },
+ windowSize: { width: window.innerWidth, height: window.innerHeight },
+ bbbVersion: Meteor.settings.public.app.bbbServerVersion,
+ location: window.location.href,
+ };
+
+ logger.info({
+ logCode: 'joinhandler_component_clientinfo',
+ extraInfo: { clientInfo },
+ },
+ 'Log information about the client');
+ };
+
+ const setAuth = (resp) => {
+ const {
+ meetingID, internalUserID, authToken, logoutUrl,
+ fullname, externUserID, confname,
+ } = resp;
+ return new Promise((resolve) => {
+ Auth.set(
+ meetingID, internalUserID, authToken, logoutUrl,
+ sessionToken, fullname, externUserID, confname,
+ );
+ resolve(resp);
+ });
+ };
+
+ const setLogoutURL = (url) => {
+ Auth.logoutURL = url;
+ return true;
+ };
+
+ const setLogoURL = (resp) => {
+ setCustomLogoUrl(resp.customLogoURL);
+ return resp;
+ };
+
+ const setModOnlyMessage = (resp) => {
+ if (resp && resp.modOnlyMessage) {
+ const sanitizedModOnlyText = SanitizeHTML(resp.modOnlyMessage, {
+ allowedTags: ['a', 'b', 'br', 'i', 'img', 'li', 'small', 'span', 'strong', 'u', 'ul'],
+ allowedAttributes: {
+ a: ['href', 'name', 'target'],
+ img: ['src', 'width', 'height'],
+ },
+ allowedSchemes: ['https'],
+ });
+ setModeratorOnlyMessage(sanitizedModOnlyText);
+ }
+ return resp;
+ };
+
+ const setCustomData = (resp) => {
+ const { customdata } = resp;
+
+ return new Promise((resolve) => {
+ if (customdata.length) {
+ makeCall('addUserSettings', customdata).then((r) => resolve(r));
+ }
+ resolve(true);
+ });
+ };
+
+ const setBannerProps = (resp) => {
+ Session.set('bannerText', resp.bannerText);
+ Session.set('bannerColor', resp.bannerColor);
+ };
+
+ const setUserDefaultLayout = (response) => {
+ const settings = {
+ application: {
+ ...Settings.application,
+ selectedLayout: LAYOUT_TYPE[response.defaultLayout] || 'custom',
+ },
+ };
+ updateSettings(settings);
+ };
+
+ // use enter api to get params for the client
+ const url = `${APP.bbbWebBase}/api/enter?sessionToken=${sessionToken}`;
+ const fetchContent = await fetch(url, { credentials: 'include' });
+ const parseToJson = await fetchContent.json();
+ const { response } = parseToJson;
+
+ setLogoutURL(response.logoutUrl);
+ logUserInfo();
+
+ if (response.returncode !== 'FAILED') {
+ await setAuth(response);
+
+ setBannerProps(response);
+ setLogoURL(response);
+ setModOnlyMessage(response);
+
+ Tracker.autorun(async (cd) => {
+ const user = CurrentUser
+ .findOne({ userId: Auth.userID, approved: true }, { fields: { _id: 1 } });
+ if (user) {
+ await setCustomData(response);
+ setUserDefaultLayout(response);
+ cd.stop();
+ }
+ });
+
+ logger.info({
+ logCode: 'joinhandler_component_joinroutehandler_success',
+ extraInfo: {
+ response,
+ },
+ }, 'User successfully went through main.joinRouteHandler');
+ } else {
+
+ if(['missingSession','meetingForciblyEnded','notFound'].includes(response.messageKey)) {
+ JoinHandler.setError('410');
+ Session.set('errorMessageDescription', 'meeting_ended');
+ } else if(response.messageKey == "guestDeny") {
+ JoinHandler.setError('401');
+ Session.set('errorMessageDescription', 'guest_deny');
+ } else if(response.messageKey == "maxParticipantsReached") {
+ JoinHandler.setError('401');
+ Session.set('errorMessageDescription', 'max_participants_reason');
+ } else {
+ JoinHandler.setError('401');
+ Session.set('errorMessageDescription', response.message);
+ }
+
+ logger.error({
+ logCode: 'joinhandler_component_joinroutehandler_error',
+ extraInfo: {
+ response,
+ error: new Error(response.message),
+ },
+ }, 'User faced an error on main.joinRouteHandler.');
+ }
+ this.setState({
+ joined: true,
+ hasAlreadyJoined: true,
+ });
+ }
+
+ render() {
+ const { children } = this.props;
+ const { joined } = this.state;
+ return joined
+ ? children
+ : ( );
+ }
+}
+
+export default JoinHandler;
+
+JoinHandler.propTypes = propTypes;
diff --git a/src/2.7.12/imports/ui/components/layout/context.jsx b/src/2.7.12/imports/ui/components/layout/context.jsx
new file mode 100644
index 00000000..d02693ed
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/context.jsx
@@ -0,0 +1,1243 @@
+import React, { useReducer } from 'react';
+import { createContext, useContextSelector } from 'use-context-selector';
+import PropTypes from 'prop-types';
+import { ACTIONS } from '/imports/ui/components/layout/enums';
+import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
+import { INITIAL_INPUT_STATE, INITIAL_OUTPUT_STATE } from './initState';
+
+// variable to debug in console log
+const debug = false;
+
+const debugActions = (action, value) => {
+ const baseStyles = [
+ 'color: #fff',
+ 'background-color: #d64541',
+ 'padding: 2px 4px',
+ 'border-radius: 2px',
+ ].join(';');
+ return debug && console.log(`%c${action}`, baseStyles, value);
+};
+
+const providerPropTypes = {
+ children: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.node),
+ PropTypes.node,
+ ]).isRequired,
+};
+
+const LayoutContextSelector = createContext();
+
+const initState = {
+ deviceType: null,
+ isRTL: false,
+ layoutType: DEFAULT_VALUES.layoutType,
+ fontSize: DEFAULT_VALUES.fontSize,
+ idChatOpen: DEFAULT_VALUES.idChatOpen,
+ fullscreen: {
+ element: '',
+ group: '',
+ },
+ input: INITIAL_INPUT_STATE,
+ output: INITIAL_OUTPUT_STATE,
+};
+
+const reducer = (state, action) => {
+ debugActions(action.type, action.value);
+ switch (action.type) {
+
+ case ACTIONS.SET_FOCUSED_CAMERA_ID: {
+ const { cameraDock } = state.input;
+ const { focusedId } = cameraDock;
+
+ if (focusedId === action.value) return state;
+
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ cameraDock: {
+ ...cameraDock,
+ focusedId: action.value,
+ },
+ },
+ };
+ }
+
+ case ACTIONS.SET_LAYOUT_INPUT: {
+ if (state.input === action.value) return state;
+ return {
+ ...state,
+ input: action.value,
+ };
+ }
+
+ case ACTIONS.SET_AUTO_ARRANGE_LAYOUT: {
+ const { autoarrAngeLayout } = state.input;
+ if (autoarrAngeLayout === action.value) return state;
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ autoarrAngeLayout: action.value,
+ },
+ };
+ }
+
+ case ACTIONS.SET_IS_RTL: {
+ const { isRTL } = state;
+ if (isRTL === action.value) return state;
+ return {
+ ...state,
+ isRTL: action.value,
+ };
+ }
+
+ // LAYOUT TYPE
+ // using to load a diferent layout manager
+ case ACTIONS.SET_LAYOUT_TYPE: {
+ const { layoutType } = state.input;
+ if (layoutType === action.value) return state;
+ return {
+ ...state,
+ layoutType: action.value,
+ };
+ }
+
+ // FONT SIZE
+ case ACTIONS.SET_FONT_SIZE: {
+ const { fontSize } = state;
+ if (fontSize === action.value) return state;
+ return {
+ ...state,
+ fontSize: action.value,
+ };
+ }
+
+ // ID CHAT open in sidebar content panel
+ case ACTIONS.SET_ID_CHAT_OPEN: {
+ if (state.idChatOpen === action.value) return state;
+ return {
+ ...state,
+ idChatOpen: action.value,
+ };
+ }
+
+ // DEVICE
+ case ACTIONS.SET_DEVICE_TYPE: {
+ const { deviceType } = state;
+ if (deviceType === action.value) return state;
+ return {
+ ...state,
+ deviceType: action.value,
+ };
+ }
+
+ // BROWSER
+ case ACTIONS.SET_BROWSER_SIZE: {
+ const { width, height } = action.value;
+ const { browser } = state.input;
+ if (browser.width === width
+ && browser.height === height) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ browser: {
+ width,
+ height,
+ },
+ },
+ };
+ }
+
+ // BANNER BAR
+ case ACTIONS.SET_HAS_BANNER_BAR: {
+ const { bannerBar } = state.input;
+ if (bannerBar.hasBanner === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ bannerBar: {
+ ...bannerBar,
+ hasBanner: action.value,
+ },
+ },
+ };
+ }
+
+ // NOTIFICATIONS BAR
+ case ACTIONS.SET_HAS_NOTIFICATIONS_BAR: {
+ const { notificationsBar } = state.input;
+ if (notificationsBar.hasNotification === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ notificationsBar: {
+ ...notificationsBar,
+ hasNotification: action.value,
+ },
+ },
+ };
+ }
+
+ // NAV BAR
+ case ACTIONS.SET_NAVBAR_OUTPUT: {
+ const {
+ display, width, height, top, left, tabOrder, zIndex,
+ } = action.value;
+ const { navBar } = state.output;
+ if (navBar.display === display
+ && navBar.width === width
+ && navBar.height === height
+ && navBar.top === top
+ && navBar.left === left
+ && navBar.zIndex === zIndex
+ && navBar.tabOrder === tabOrder) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ navBar: {
+ ...navBar,
+ display,
+ width,
+ height,
+ top,
+ left,
+ tabOrder,
+ zIndex,
+ },
+ },
+ };
+ }
+
+ // ACTION BAR
+ case ACTIONS.SET_ACTIONBAR_OUTPUT: {
+ const {
+ display, width, height, innerHeight, top, left, padding, tabOrder, zIndex,
+ } = action.value;
+ const { actionBar } = state.output;
+ if (actionBar.display === display
+ && actionBar.width === width
+ && actionBar.height === height
+ && actionBar.innerHeight === innerHeight
+ && actionBar.top === top
+ && actionBar.left === left
+ && actionBar.padding === padding
+ && actionBar.zIndex === zIndex
+ && actionBar.tabOrder === tabOrder) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ actionBar: {
+ ...actionBar,
+ display,
+ width,
+ height,
+ innerHeight,
+ top,
+ left,
+ padding,
+ tabOrder,
+ zIndex,
+ },
+ },
+ };
+ }
+
+ // CAPTIONS
+ case ACTIONS.SET_CAPTIONS_OUTPUT: {
+ const {
+ left, right, maxWidth,
+ } = action.value;
+ const { captions } = state.output;
+ if (captions.left === left
+ && captions.right === right
+ && captions.maxWidth === maxWidth) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ captions: {
+ ...captions,
+ left,
+ right,
+ maxWidth,
+ },
+ },
+ };
+ }
+
+ // SIDEBAR NAVIGATION
+ case ACTIONS.SET_SIDEBAR_NAVIGATION_IS_OPEN: {
+ const { sidebarNavigation } = state.input;
+ if (sidebarNavigation.isOpen === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ sidebarNavigation: {
+ ...sidebarNavigation,
+ isOpen: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_SIDEBAR_NAVIGATION_PANEL: {
+ const { sidebarNavigation } = state.input;
+ if (sidebarNavigation.sidebarNavPanel === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ sidebarNavigation: {
+ ...sidebarNavigation,
+ sidebarNavPanel: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_SIDEBAR_NAVIGATION_SIZE: {
+ const { width, browserWidth } = action.value;
+ const { sidebarNavigation } = state.input;
+ if (sidebarNavigation.width === width
+ && sidebarNavigation.browserWidth === browserWidth) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ sidebarNavigation: {
+ ...sidebarNavigation,
+ width,
+ browserWidth,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_SIDEBAR_NAVIGATION_OUTPUT: {
+ const {
+ display,
+ minWidth,
+ width,
+ maxWidth,
+ minHeight,
+ height,
+ maxHeight,
+ top,
+ left,
+ right,
+ tabOrder,
+ isResizable,
+ zIndex,
+ } = action.value;
+ const { sidebarNavigation } = state.output;
+ if (sidebarNavigation.display === display
+ && sidebarNavigation.minWidth === minWidth
+ && sidebarNavigation.maxWidth === maxWidth
+ && sidebarNavigation.width === width
+ && sidebarNavigation.minHeight === minHeight
+ && sidebarNavigation.height === height
+ && sidebarNavigation.maxHeight === maxHeight
+ && sidebarNavigation.top === top
+ && sidebarNavigation.left === left
+ && sidebarNavigation.right === right
+ && sidebarNavigation.tabOrder === tabOrder
+ && sidebarNavigation.zIndex === zIndex
+ && sidebarNavigation.isResizable === isResizable) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ sidebarNavigation: {
+ ...sidebarNavigation,
+ display,
+ minWidth,
+ width,
+ maxWidth,
+ minHeight,
+ height,
+ maxHeight,
+ top,
+ left,
+ right,
+ tabOrder,
+ isResizable,
+ zIndex,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_SIDEBAR_NAVIGATION_RESIZABLE_EDGE: {
+ const {
+ top, right, bottom, left,
+ } = action.value;
+ const { sidebarNavigation } = state.output;
+ if (sidebarNavigation.resizableEdge.top === top
+ && sidebarNavigation.resizableEdge.right === right
+ && sidebarNavigation.resizableEdge.bottom === bottom
+ && sidebarNavigation.resizableEdge.left === left) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ sidebarNavigation: {
+ ...sidebarNavigation,
+ resizableEdge: {
+ top,
+ right,
+ bottom,
+ left,
+ },
+ },
+ },
+ };
+ }
+
+ // SIDEBAR CONTENT
+ case ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN: {
+ const { sidebarContent, sidebarNavigation } = state.input;
+ if (sidebarContent.isOpen === action.value) {
+ return state;
+ }
+ // When opening content sidebar, the navigation sidebar should be opened as well
+ if (action.value === true) sidebarNavigation.isOpen = true;
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ sidebarNavigation,
+ sidebarContent: {
+ ...sidebarContent,
+ isOpen: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_SIDEBAR_CONTENT_PANEL: {
+ const { sidebarContent } = state.input;
+ if (sidebarContent.sidebarContentPanel === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ sidebarContent: {
+ ...sidebarContent,
+ sidebarContentPanel: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_SIDEBAR_CONTENT_SIZE: {
+ const {
+ width,
+ browserWidth,
+ height,
+ browserHeight,
+ } = action.value;
+ const { sidebarContent } = state.input;
+ if (sidebarContent.width === width
+ && sidebarContent.browserWidth === browserWidth
+ && sidebarContent.height === height
+ && sidebarContent.browserHeight === browserHeight) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ sidebarContent: {
+ ...sidebarContent,
+ width,
+ browserWidth,
+ height,
+ browserHeight,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_SIDEBAR_CONTENT_OUTPUT: {
+ const {
+ display,
+ minWidth,
+ width,
+ maxWidth,
+ minHeight,
+ height,
+ maxHeight,
+ top,
+ left,
+ right,
+ currentPanelType,
+ tabOrder,
+ isResizable,
+ zIndex,
+ } = action.value;
+ const { sidebarContent } = state.output;
+ if (sidebarContent.display === display
+ && sidebarContent.minWidth === minWidth
+ && sidebarContent.width === width
+ && sidebarContent.maxWidth === maxWidth
+ && sidebarContent.minHeight === minHeight
+ && sidebarContent.height === height
+ && sidebarContent.maxHeight === maxHeight
+ && sidebarContent.top === top
+ && sidebarContent.left === left
+ && sidebarContent.right === right
+ && sidebarContent.tabOrder === tabOrder
+ && sidebarContent.zIndex === zIndex
+ && sidebarContent.isResizable === isResizable) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ sidebarContent: {
+ ...sidebarContent,
+ display,
+ minWidth,
+ width,
+ maxWidth,
+ minHeight,
+ height,
+ maxHeight,
+ top,
+ left,
+ right,
+ currentPanelType,
+ tabOrder,
+ isResizable,
+ zIndex,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_SIDEBAR_CONTENT_RESIZABLE_EDGE: {
+ const {
+ top, right, bottom, left,
+ } = action.value;
+ const { sidebarContent } = state.output;
+ if (sidebarContent.resizableEdge.top === top
+ && sidebarContent.resizableEdge.right === right
+ && sidebarContent.resizableEdge.bottom === bottom
+ && sidebarContent.resizableEdge.left === left) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ sidebarContent: {
+ ...sidebarContent,
+ resizableEdge: {
+ top,
+ right,
+ bottom,
+ left,
+ },
+ },
+ },
+ };
+ }
+
+ // MEDIA
+ case ACTIONS.SET_MEDIA_AREA_SIZE: {
+ const { width, height } = action.value;
+ const { mediaArea } = state.output;
+ if (mediaArea.width === width
+ && mediaArea.height === height) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ mediaArea: {
+ ...mediaArea,
+ width,
+ height,
+ },
+ },
+ };
+ }
+
+ // WEBCAMS
+ case ACTIONS.SET_NUM_CAMERAS: {
+ const { cameraDock } = state.input;
+ if (cameraDock.numCameras === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ cameraDock: {
+ ...cameraDock,
+ numCameras: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_CAMERA_DOCK_IS_DRAGGING: {
+ const { cameraDock } = state.input;
+ if (cameraDock.isDragging === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ cameraDock: {
+ ...cameraDock,
+ isDragging: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_CAMERA_DOCK_IS_RESIZING: {
+ const { cameraDock } = state.input;
+ if (cameraDock.isResizing === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ cameraDock: {
+ ...cameraDock,
+ isResizing: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_CAMERA_DOCK_POSITION: {
+ const { cameraDock } = state.input;
+ if (cameraDock.position === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ cameraDock: {
+ ...cameraDock,
+ position: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_CAMERA_DOCK_SIZE: {
+ const {
+ width, height, browserWidth, browserHeight,
+ } = action.value;
+ const { cameraDock } = state.input;
+ if (cameraDock.width === width
+ && cameraDock.height === height
+ && cameraDock.browserWidth === browserWidth
+ && cameraDock.browserHeight === browserHeight) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ cameraDock: {
+ ...cameraDock,
+ width,
+ height,
+ browserWidth,
+ browserHeight,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_CAMERA_DOCK_OPTIMAL_GRID_SIZE: {
+ const { width, height } = action.value;
+ const { cameraDock } = state.input;
+ const { cameraOptimalGridSize } = cameraDock;
+ if (cameraOptimalGridSize.width === width
+ && cameraOptimalGridSize.height === height) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ cameraDock: {
+ ...cameraDock,
+ cameraOptimalGridSize: {
+ width,
+ height,
+ },
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_CAMERA_DOCK_OUTPUT: {
+ const {
+ display,
+ position,
+ minWidth,
+ width,
+ maxWidth,
+ presenterMaxWidth,
+ minHeight,
+ height,
+ maxHeight,
+ top,
+ left,
+ right,
+ tabOrder,
+ isDraggable,
+ resizableEdge,
+ zIndex,
+ focusedId,
+ } = action.value;
+ const { cameraDock } = state.output;
+ if (cameraDock.display === display
+ && cameraDock.position === position
+ && cameraDock.width === width
+ && cameraDock.maxWidth === maxWidth
+ && cameraDock.presenterMaxWidth === presenterMaxWidth
+ && cameraDock.height === height
+ && cameraDock.maxHeight === maxHeight
+ && cameraDock.top === top
+ && cameraDock.left === left
+ && cameraDock.right === right
+ && cameraDock.tabOrder === tabOrder
+ && cameraDock.isDraggable === isDraggable
+ && cameraDock.zIndex === zIndex
+ && cameraDock.resizableEdge === resizableEdge
+ && cameraDock.focusedId === focusedId) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ cameraDock: {
+ ...cameraDock,
+ display,
+ position,
+ minWidth,
+ width,
+ maxWidth,
+ presenterMaxWidth,
+ minHeight,
+ height,
+ maxHeight,
+ top,
+ left,
+ right,
+ tabOrder,
+ isDraggable,
+ resizableEdge,
+ zIndex,
+ focusedId,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_CAMERA_DOCK_IS_DRAGGABLE: {
+ const { cameraDock } = state.output;
+ if (cameraDock.isDraggable === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ cameraDock: {
+ ...cameraDock,
+ isDraggable: action.value,
+ },
+ },
+ };
+ }
+
+ // WEBCAMS DROP AREAS
+ case ACTIONS.SET_DROP_AREAS: {
+ const { dropZoneAreas } = state.output;
+ if (dropZoneAreas === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ dropZoneAreas: action.value,
+ },
+ };
+ }
+
+ // PRESENTATION
+ case ACTIONS.SET_PRESENTATION_IS_OPEN: {
+ const { presentation } = state.input;
+ if (presentation.isOpen === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ presentation: {
+ ...presentation,
+ isOpen: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_PRESENTATION_SLIDES_LENGTH: {
+ const { presentation } = state.input;
+ if (presentation.slidesLength === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ presentation: {
+ ...presentation,
+ slidesLength: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_PRESENTATION_NUM_CURRENT_SLIDE: {
+ const { presentation } = state.input;
+ if (presentation.currentSlide.num === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ presentation: {
+ ...presentation,
+ currentSlide: {
+ ...presentation.currentSlide,
+ num: action.value,
+ },
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_PRESENTATION_CURRENT_SLIDE_SIZE: {
+ const { width, height } = action.value;
+ const { presentation } = state.input;
+ const { currentSlide } = presentation;
+ if (currentSlide.size.width === width
+ && currentSlide.size.height === height) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ presentation: {
+ ...presentation,
+ currentSlide: {
+ ...currentSlide,
+ size: {
+ width,
+ height,
+ },
+ },
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_PRESENTATION_RESIZABLE_EDGE: {
+ const {
+ top, right, bottom, left,
+ } = action.value;
+ const { presentation } = state.output;
+ const { resizableEdge } = presentation;
+ if (resizableEdge.top === top
+ && resizableEdge.right === right
+ && resizableEdge.bottom === bottom
+ && resizableEdge.left === left) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ presentation: {
+ ...presentation,
+ resizableEdge: {
+ top,
+ right,
+ bottom,
+ left,
+ },
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_PRESENTATION_SIZE: {
+ const {
+ width, height, browserWidth, browserHeight,
+ } = action.value;
+ const { presentation } = state.input;
+ if (presentation.width === width
+ && presentation.height === height
+ && presentation.browserWidth === browserWidth
+ && presentation.browserHeight === browserHeight) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ presentation: {
+ ...presentation,
+ width,
+ height,
+ browserWidth,
+ browserHeight,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_PRESENTATION_OUTPUT: {
+ const {
+ display,
+ minWidth,
+ width,
+ maxWidth,
+ minHeight,
+ height,
+ maxHeight,
+ top,
+ left,
+ right,
+ tabOrder,
+ isResizable,
+ zIndex,
+ } = action.value;
+ const { presentation } = state.output;
+ if (presentation.display === display
+ && presentation.minWidth === minWidth
+ && presentation.width === width
+ && presentation.maxWidth === maxWidth
+ && presentation.minHeight === minHeight
+ && presentation.height === height
+ && presentation.maxHeight === maxHeight
+ && presentation.top === top
+ && presentation.left === left
+ && presentation.right === right
+ && presentation.tabOrder === tabOrder
+ && presentation.zIndex === zIndex
+ && presentation.isResizable === isResizable) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ presentation: {
+ ...presentation,
+ display,
+ minWidth,
+ width,
+ maxWidth,
+ minHeight,
+ height,
+ maxHeight,
+ top,
+ left,
+ right,
+ tabOrder,
+ isResizable,
+ zIndex,
+ },
+ },
+ };
+ }
+
+ // FULLSCREEN
+ case ACTIONS.SET_FULLSCREEN_ELEMENT: {
+ const { fullscreen } = state;
+ if (fullscreen.element === action.value.element
+ && fullscreen.group === action.value.group) {
+ return state;
+ }
+ return {
+ ...state,
+ fullscreen: {
+ element: action.value.element,
+ group: action.value.group,
+ },
+ };
+ }
+
+ // SCREEN SHARE
+ case ACTIONS.SET_HAS_SCREEN_SHARE: {
+ const { screenShare } = state.input;
+ if (screenShare.hasScreenShare === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ screenShare: {
+ ...screenShare,
+ hasScreenShare: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_SCREEN_SHARE_SIZE: {
+ const {
+ width, height, browserWidth, browserHeight,
+ } = action.value;
+ const { screenShare } = state.input;
+ if (screenShare.width === width
+ && screenShare.height === height
+ && screenShare.browserWidth === browserWidth
+ && screenShare.browserHeight === browserHeight) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ screenShare: {
+ ...screenShare,
+ width,
+ height,
+ browserWidth,
+ browserHeight,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_SCREEN_SHARE_OUTPUT: {
+ const {
+ width,
+ height,
+ top,
+ left,
+ right,
+ zIndex,
+ } = action.value;
+ const { screenShare } = state.output;
+ if (screenShare.width === width
+ && screenShare.height === height
+ && screenShare.top === top
+ && screenShare.left === left
+ && screenShare.right === right
+ && screenShare.zIndex === zIndex) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ screenShare: {
+ ...screenShare,
+ width,
+ height,
+ top,
+ left,
+ right,
+ zIndex,
+ },
+ },
+ };
+ }
+
+ // EXTERNAL VIDEO
+ case ACTIONS.SET_HAS_EXTERNAL_VIDEO: {
+ const { externalVideo } = state.input;
+ if (externalVideo.hasExternalVideo === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ externalVideo: {
+ ...externalVideo,
+ hasExternalVideo: action.value,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_EXTERNAL_VIDEO_SIZE: {
+ const {
+ width, height, browserWidth, browserHeight,
+ } = action.value;
+ const { externalVideo } = state.input;
+ if (externalVideo.width === width
+ && externalVideo.height === height
+ && externalVideo.browserWidth === browserWidth
+ && externalVideo.browserHeight === browserHeight) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ externalVideo: {
+ ...externalVideo,
+ width,
+ height,
+ browserWidth,
+ browserHeight,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_EXTERNAL_VIDEO_OUTPUT: {
+ const {
+ width,
+ height,
+ top,
+ left,
+ right,
+ } = action.value;
+ const { externalVideo } = state.output;
+ if (externalVideo.width === width
+ && externalVideo.height === height
+ && externalVideo.top === top
+ && externalVideo.left === left
+ && externalVideo.right === right) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ externalVideo: {
+ ...externalVideo,
+ width,
+ height,
+ top,
+ left,
+ right,
+ },
+ },
+ };
+ }
+
+ // NOTES
+ case ACTIONS.SET_SHARED_NOTES_OUTPUT: {
+ const {
+ width,
+ height,
+ top,
+ left,
+ right,
+ } = action.value;
+ const { sharedNotes } = state.output;
+ if (sharedNotes.width === width
+ && sharedNotes.height === height
+ && sharedNotes.top === top
+ && sharedNotes.left === left
+ && sharedNotes.right === right) {
+ return state;
+ }
+ return {
+ ...state,
+ output: {
+ ...state.output,
+ sharedNotes: {
+ ...sharedNotes,
+ width,
+ height,
+ top,
+ left,
+ right,
+ },
+ },
+ };
+ }
+ case ACTIONS.SET_NOTES_IS_PINNED: {
+ const { sharedNotes } = state.input;
+ if (sharedNotes.isPinned === action.value) {
+ return state;
+ }
+ return {
+ ...state,
+ input: {
+ ...state.input,
+ sharedNotes: {
+ ...sharedNotes,
+ isPinned: action.value,
+ },
+ },
+ };
+ }
+ default: {
+ throw new Error('Unexpected action');
+ }
+ }
+};
+
+const LayoutContextProvider = (props) => {
+ const [layoutContextState, layoutContextDispatch] = useReducer(reducer, initState);
+ const { children } = props;
+ return (
+
+ {children}
+
+ );
+};
+LayoutContextProvider.propTypes = providerPropTypes;
+
+const layoutSelect = (selector) => {
+ return useContextSelector(LayoutContextSelector, layout => selector(layout[0]));
+};
+const layoutSelectInput = (selector) => {
+ return useContextSelector(LayoutContextSelector, layout => selector(layout[0].input));
+};
+const layoutSelectOutput = (selector) => {
+ return useContextSelector(LayoutContextSelector, layout => selector(layout[0].output));
+};
+const layoutDispatch = () => {
+ return useContextSelector(LayoutContextSelector, layout => layout[1]);
+};
+
+export {
+ LayoutContextProvider,
+ layoutSelect,
+ layoutSelectInput,
+ layoutSelectOutput,
+ layoutDispatch,
+}
diff --git a/src/2.7.12/imports/ui/components/layout/defaultValues.js b/src/2.7.12/imports/ui/components/layout/defaultValues.js
new file mode 100644
index 00000000..eb8743e0
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/defaultValues.js
@@ -0,0 +1,55 @@
+import { Meteor } from 'meteor/meteor';
+import { LAYOUT_TYPE, CAMERADOCK_POSITION, PANELS } from './enums';
+
+const CHAT_CONFIG = Meteor.settings.public.chat;
+const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
+
+const DEFAULT_VALUES = {
+ layoutType: LAYOUT_TYPE.CUSTOM_LAYOUT,
+ panelType: 'chat',
+ idChatOpen: PUBLIC_CHAT_ID,
+ fontSize: 16,
+
+ cameraPosition: CAMERADOCK_POSITION.CONTENT_TOP,
+ cameraDockTabOrder: 4,
+ cameraDockMinHeight: 120,
+ cameraDockMinWidth: 120,
+ camerasMargin: 10,
+ captionsMargin: 10,
+
+ presentationTabOrder: 5,
+ presentationMinHeight: 220,
+ presentationToolbarMinWidth: 430,
+
+ bannerHeight: 34,
+
+ navBarHeight: 85,
+ navBarTop: 0,
+ navBarTabOrder: 3,
+
+ actionBarHeight: 42,
+ actionBarPadding: 11.2,
+ actionBarTabOrder: 6,
+
+ sidebarNavMaxWidth: 240,
+ sidebarNavMinWidth: 70,
+ sidebarNavHeight: '100%',
+ sidebarNavTop: 0,
+ sidebarNavLeft: 0,
+ sidebarNavTabOrder: 1,
+ sidebarNavPanel: PANELS.USERLIST,
+
+ sidebarContentMaxWidth: 800,
+ sidebarContentMinWidth: 70,
+ sidebarContentMinHeight: 200,
+ sidebarContentHeight: '100%',
+ sidebarContentTop: 0,
+ sidebarContentTabOrder: 2,
+ sidebarContentPanel: PANELS.NONE,
+};
+
+export default DEFAULT_VALUES;
+export {
+ LAYOUT_TYPE,
+ CAMERADOCK_POSITION,
+};
diff --git a/src/2.7.12/imports/ui/components/layout/enums.js b/src/2.7.12/imports/ui/components/layout/enums.js
new file mode 100644
index 00000000..4f9f5c4a
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/enums.js
@@ -0,0 +1,112 @@
+export const LAYOUT_TYPE = {
+ CUSTOM_LAYOUT: 'custom',
+ SMART_LAYOUT: 'smart',
+ PRESENTATION_FOCUS: 'presentationFocus',
+ VIDEO_FOCUS: 'videoFocus',
+};
+
+export const DEVICE_TYPE = {
+ MOBILE: 'mobile',
+ TABLET_PORTRAIT: 'tablet_portrait',
+ TABLET_LANDSCAPE: 'tablet_landscape',
+ TABLET: 'tablet',
+ DESKTOP: 'desktop',
+};
+
+export const SMALL_VIEWPORT_BREAKPOINT = 640;
+
+export const CAMERADOCK_POSITION = {
+ CONTENT_TOP: 'contentTop',
+ CONTENT_RIGHT: 'contentRight',
+ CONTENT_BOTTOM: 'contentBottom',
+ CONTENT_LEFT: 'contentLeft',
+ SIDEBAR_CONTENT_BOTTOM: 'sidebarContentBottom',
+};
+
+export const ACTIONS = {
+ SET_AUTO_ARRANGE_LAYOUT: 'setAutoArrangeLayout',
+ SET_IS_RTL: 'setIsRTL',
+ SET_LAYOUT_TYPE: 'setLayoutType',
+ SET_DEVICE_TYPE: 'setDeviceType',
+ SET_FONT_SIZE: 'setFontSize',
+
+ SET_FOCUSED_CAMERA_ID: 'focusedId',
+
+ SET_LAYOUT_INPUT: 'setLayoutInput',
+
+ SET_SIDEBAR_NAVIGATION_PANEL: 'setSidebarNavigationPanel',
+ SET_SIDEBAR_CONTENT_PANEL: 'setSidebarcontentPanel',
+
+ SET_ID_CHAT_OPEN: 'setIdChatOpen',
+
+ SET_BROWSER_SIZE: 'setBrowserSize',
+
+ SET_HAS_BANNER_BAR: 'setHasBannerBar',
+ SET_HAS_NOTIFICATIONS_BAR: 'setHasNotificationsBar',
+
+ SET_NAVBAR_OUTPUT: 'setNavBarOutput',
+
+ SET_ACTIONBAR_OUTPUT: 'setActionBarOutput',
+
+ SET_SIDEBAR_NAVIGATION_IS_OPEN: 'setSidebarNavigationIsOpen',
+ SET_SIDEBAR_NAVIGATION_SIZE: 'setSidebarNavigationSize',
+ SET_SIDEBAR_NAVIGATION_OUTPUT: 'setSidebarNavigationOutput',
+ SET_SIDEBAR_NAVIGATION_IS_RESIZABLE: 'setSidebarNavigationIsResizable',
+ SET_SIDEBAR_NAVIGATION_RESIZABLE_EDGE: 'setSidebarNavigationResizableEdge',
+
+ SET_SIDEBAR_CONTENT_IS_OPEN: 'setSidebarContentIsOpen',
+ SET_SIDEBAR_CONTENT_SIZE: 'setSidebarContentSize',
+ SET_SIDEBAR_CONTENT_PANEL_TYPE: 'setSidebarContentPanelType',
+ SET_SIDEBAR_CONTENT_OUTPUT: 'setSidebarContentOutput',
+ SET_SIDEBAR_CONTENT_IS_RESIZABLE: 'setSidebarContentIsResizable',
+ SET_SIDEBAR_CONTENT_RESIZABLE_EDGE: 'setSidebarContentResizableEdge',
+
+ SET_MEDIA_AREA_SIZE: 'setMediaAreaSize',
+
+ SET_NUM_CAMERAS: 'setNumCameras',
+ SET_CAMERA_DOCK_IS_DRAGGING: 'setCameraDockIsDragging',
+ SET_CAMERA_DOCK_IS_RESIZING: 'setCameraDockIsResizing',
+ SET_CAMERA_DOCK_POSITION: 'setCameraDockPosition',
+ SET_CAMERA_DOCK_SIZE: 'setCameraDockSize',
+ SET_CAMERA_DOCK_OPTIMAL_GRID_SIZE: 'setCameraDockOptimalGridSize',
+ SET_CAMERA_DOCK_OUTPUT: 'setCameraDockOutput',
+ SET_CAMERA_DOCK_IS_DRAGGABLE: 'setCameraDockIsDraggable',
+ SET_CAMERA_DOCK_IS_RESIZABLE: 'setCameraDockIsResizable',
+ SET_CAMERA_DOCK_RESIZABLE_EDGE: 'setCameraDockResizableEdge',
+
+ SET_DROP_AREAS: 'setDropAreas',
+
+ SET_PRESENTATION_IS_OPEN: 'setPresentationIsOpen',
+ SET_PRESENTATION_CURRENT_SLIDE_SIZE: 'setPresentationCurrentSlideSize',
+ SET_PRESENTATION_NUM_CURRENT_SLIDE: 'setPresentationNumCurrentSlide',
+ SET_PRESENTATION_SLIDES_LENGTH: 'setPresentationSlidesLength',
+ SET_PRESENTATION_SIZE: 'setPresentationSize',
+ SET_PRESENTATION_OUTPUT: 'setPresentationOutput',
+ SET_PRESENTATION_IS_RESIZABLE: 'setPresentationIsResizable',
+ SET_PRESENTATION_RESIZABLE_EDGE: 'setPresentationResizableEdge',
+
+ SET_FULLSCREEN_ELEMENT: 'setFullscreenElement',
+
+ SET_HAS_SCREEN_SHARE: 'setHasScreenShare',
+ SET_SCREEN_SHARE_SIZE: 'setScreenShareSize',
+ SET_SCREEN_SHARE_OUTPUT: 'setScreenShareOutput',
+
+ SET_HAS_EXTERNAL_VIDEO: 'setHasExternalVideo',
+ SET_EXTERNAL_VIDEO_SIZE: 'setExternalVideoSize',
+ SET_EXTERNAL_VIDEO_OUTPUT: 'setExternalVideoOutput',
+
+ SET_SHARED_NOTES_OUTPUT: 'setSharedNotesOutput',
+ SET_NOTES_IS_PINNED: 'setNotesIsPinned',
+};
+
+export const PANELS = {
+ USERLIST: 'userlist',
+ CHAT: 'chat',
+ POLL: 'poll',
+ CAPTIONS: 'captions',
+ BREAKOUT: 'breakoutroom',
+ SHARED_NOTES: 'shared-notes',
+ TIMER: 'timer',
+ WAITING_USERS: 'waiting-users',
+ NONE: 'none',
+};
diff --git a/src/2.7.12/imports/ui/components/layout/initState.js b/src/2.7.12/imports/ui/components/layout/initState.js
new file mode 100644
index 00000000..14a199c8
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/initState.js
@@ -0,0 +1,246 @@
+import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
+
+export const INITIAL_INPUT_STATE = {
+ autoarrAngeLayout: true,
+ customParameters: {
+
+ },
+ browser: {
+ width: window.document.documentElement.clientWidth,
+ height: window.document.documentElement.clientHeight,
+ },
+ bannerBar: {
+ hasBanner: false,
+ },
+ notificationsBar: {
+ hasNotification: false,
+ },
+ navBar: {
+ hasNavBar: true,
+ height: DEFAULT_VALUES.navBarHeight,
+ },
+ actionBar: {
+ hasActionBar: true,
+ height: DEFAULT_VALUES.actionBarHeight,
+ },
+ sidebarNavigation: {
+ isOpen: true,
+ width: 0,
+ height: 0,
+ browserWidth: 0,
+ sidebarNavPanel: DEFAULT_VALUES.sidebarNavPanel,
+ },
+ sidebarContent: {
+ isOpen: true,
+ currentPanelType: DEFAULT_VALUES.panelType,
+ width: 0,
+ height: 0,
+ browserWidth: 0,
+ sidebarContentPanel: DEFAULT_VALUES.sidebarContentPanel,
+ resizableEdge: {
+ top: false,
+ right: false,
+ bottom: false,
+ left: false,
+ },
+ },
+ sidebarContentHorizontalResizer: {
+ isOpen: true,
+ currentPanelType: DEFAULT_VALUES.panelType,
+ width: 0,
+ height: 0,
+ browserWidth: 0,
+ },
+ cameraDock: {
+ numCameras: 0,
+ position: DEFAULT_VALUES.cameraPosition,
+ width: 0,
+ height: 0,
+ browserWidth: 0,
+ browserHeight: 0,
+ isDragging: false,
+ isResizing: false,
+ cameraOptimalGridSize: {
+ width: 0,
+ height: 0,
+ },
+ focusedId: 'none',
+ },
+ presentation: {
+ isOpen: true,
+ slidesLength: 0,
+ currentSlide: {
+ num: 0,
+ size: {
+ width: 0,
+ height: 0,
+ },
+ },
+ width: 0,
+ height: 0,
+ browserWidth: 0,
+ browserHeight: 0,
+ },
+ screenShare: {
+ hasScreenShare: false,
+ width: 0,
+ height: 0,
+ browserWidth: 0,
+ browserHeight: 0,
+ },
+ externalVideo: {
+ hasExternalVideo: false,
+ width: 0,
+ height: 0,
+ browserWidth: 0,
+ browserHeight: 0,
+ },
+ sharedNotes: {
+ isPinned: false,
+ width: 0,
+ height: 0,
+ browserWidth: 0,
+ browserHeight: 0,
+ },
+};
+
+export const INITIAL_OUTPUT_STATE = {
+ navBar: {
+ display: false,
+ width: 0,
+ height: 0,
+ top: 0,
+ left: 0,
+ tabOrder: 0,
+ zIndex: 1,
+ },
+ actionBar: {
+ display: false,
+ width: 0,
+ height: 0,
+ top: 0,
+ left: 0,
+ tabOrder: 0,
+ zIndex: 1,
+ },
+ captions: {
+ left: 0,
+ right: 0,
+ },
+ sidebarNavigation: {
+ display: true,
+ minWidth: 0,
+ width: 0,
+ maxWidth: 0,
+ minHeight: 0,
+ height: 0,
+ maxHeight: 0,
+ top: 0,
+ left: 0,
+ tabOrder: 0,
+ isResizable: false,
+ resizableEdge: {
+ top: false,
+ right: false,
+ bottom: false,
+ left: false,
+ },
+ zIndex: 1,
+ },
+ sidebarContent: {
+ display: true,
+ minWidth: 0,
+ width: 0,
+ maxWidth: 0,
+ minHeight: 0,
+ height: 0,
+ maxHeight: 0,
+ top: 0,
+ left: 0,
+ currentPanelType: '',
+ tabOrder: 0,
+ isResizable: false,
+ resizableEdge: {
+ top: false,
+ right: false,
+ bottom: false,
+ left: false,
+ },
+ zIndex: 1,
+ },
+ mediaArea: {
+ width: 0,
+ height: 0,
+ },
+ cameraDock: {
+ display: false,
+ position: null,
+ minWidth: 0,
+ width: 0,
+ maxWidth: 0,
+ minHeight: 0,
+ height: 0,
+ maxHeight: 0,
+ top: 0,
+ left: 0,
+ tabOrder: 0,
+ isDraggable: false,
+ isResizable: false,
+ resizableEdge: {
+ top: false,
+ right: false,
+ bottom: false,
+ left: false,
+ },
+ zIndex: 1,
+ focusedId: 'none',
+ },
+ dropZoneAreas: {},
+ presentation: {
+ display: true,
+ minWidth: 0,
+ width: 0,
+ maxWidth: 0,
+ minHeight: 0,
+ height: 0,
+ maxHeight: 0,
+ top: 0,
+ left: 0,
+ tabOrder: 0,
+ isResizable: false,
+ resizableEdge: {
+ top: false,
+ right: false,
+ bottom: false,
+ left: false,
+ },
+ presentationOrientation: 'horizontal',
+ zIndex: 1,
+ },
+ screenShare: {
+ display: false,
+ width: 0,
+ height: 0,
+ top: 0,
+ left: 0,
+ zIndex: 1,
+ },
+ externalVideo: {
+ display: false,
+ width: 0,
+ height: 0,
+ top: 0,
+ left: 0,
+ tabOrder: 0,
+ zIndex: 1,
+ },
+ sharedNotes: {
+ display: false,
+ width: 0,
+ height: 0,
+ top: 0,
+ left: 0,
+ tabOrder: 0,
+ zIndex: 1,
+ },
+};
diff --git a/src/2.7.12/imports/ui/components/layout/layout-manager/customLayout.jsx b/src/2.7.12/imports/ui/components/layout/layout-manager/customLayout.jsx
new file mode 100644
index 00000000..1e98edd4
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/layout-manager/customLayout.jsx
@@ -0,0 +1,754 @@
+import { useEffect, useRef } from 'react';
+import { throttle } from '/imports/utils/throttle';
+import { layoutSelect, layoutSelectInput, layoutDispatch } from '/imports/ui/components/layout/context';
+import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
+import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
+import { ACTIONS, CAMERADOCK_POSITION, PANELS } from '../enums';
+import Storage from '/imports/ui/services/storage/session';
+import { defaultsDeep } from '/imports/utils/array-utils';
+import { isPresentationEnabled } from '/imports/ui/services/features';
+
+const windowWidth = () => window.document.documentElement.clientWidth;
+const windowHeight = () => window.document.documentElement.clientHeight;
+const min = (value1, value2) => (value1 <= value2 ? value1 : value2);
+const max = (value1, value2) => (value1 >= value2 ? value1 : value2);
+
+const CustomLayout = (props) => {
+ const { bannerAreaHeight, calculatesActionbarHeight, isMobile } = props;
+
+ function usePrevious(value) {
+ const ref = useRef();
+ useEffect(() => {
+ ref.current = value;
+ });
+ return ref.current;
+ }
+
+ const input = layoutSelect((i) => i.input);
+ const deviceType = layoutSelect((i) => i.deviceType);
+ const isRTL = layoutSelect((i) => i.isRTL);
+ const fullscreen = layoutSelect((i) => i.fullscreen);
+ const fontSize = layoutSelect((i) => i.fontSize);
+ const currentPanelType = layoutSelect((i) => i.currentPanelType);
+
+ const presentationInput = layoutSelectInput((i) => i.presentation);
+ const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
+ const screenShareInput = layoutSelectInput((i) => i.screenShare);
+ const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
+
+ const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
+ const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
+ const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
+ const actionbarInput = layoutSelectInput((i) => i.actionBar);
+ const navbarInput = layoutSelectInput((i) => i.navBar);
+ const layoutContextDispatch = layoutDispatch();
+
+ const { isResizing } = cameraDockInput;
+
+ const prevDeviceType = usePrevious(deviceType);
+ const prevIsResizing = usePrevious(isResizing);
+
+ const throttledCalculatesLayout = throttle(() => calculatesLayout(),
+ 50, { trailing: true, leading: true });
+
+ useEffect(() => {
+ window.addEventListener('resize', () => {
+ layoutContextDispatch({
+ type: ACTIONS.SET_BROWSER_SIZE,
+ value: {
+ width: window.document.documentElement.clientWidth,
+ height: window.document.documentElement.clientHeight,
+ },
+ });
+ });
+ }, []);
+
+ useEffect(() => {
+ if (deviceType === null) return () => null;
+
+ if (deviceType !== prevDeviceType) {
+ // reset layout if deviceType changed
+ // not all options is supported in all devices
+ init();
+ } else {
+ throttledCalculatesLayout();
+ }
+ }, [input, deviceType, isRTL, fontSize, fullscreen]);
+
+ const calculatesDropAreas = (sidebarNavWidth, sidebarContentWidth, cameraDockBounds) => {
+ const { height: actionBarHeight } = calculatesActionbarHeight();
+ const mediaAreaHeight = windowHeight() - (DEFAULT_VALUES.navBarHeight + actionBarHeight);
+ const mediaAreaWidth = windowWidth() - (sidebarNavWidth + sidebarContentWidth);
+ const DROP_ZONE_DEFAUL_SIZE = 100;
+ const dropZones = {};
+ const sidebarSize = sidebarNavWidth + sidebarContentWidth;
+
+ dropZones[CAMERADOCK_POSITION.CONTENT_TOP] = {
+ top: DEFAULT_VALUES.navBarHeight,
+ left: !isRTL ? sidebarSize : null,
+ right: isRTL ? sidebarSize : null,
+ width: mediaAreaWidth,
+ height: DROP_ZONE_DEFAUL_SIZE,
+ zIndex: cameraDockBounds.zIndex,
+ };
+
+ dropZones[CAMERADOCK_POSITION.CONTENT_RIGHT] = {
+ top: DEFAULT_VALUES.navBarHeight + DROP_ZONE_DEFAUL_SIZE,
+ left: !isRTL ? windowWidth() - DROP_ZONE_DEFAUL_SIZE : 0,
+ height: mediaAreaHeight - 2 * DROP_ZONE_DEFAUL_SIZE,
+ width: DROP_ZONE_DEFAUL_SIZE,
+ zIndex: cameraDockBounds.zIndex,
+ };
+
+ dropZones[CAMERADOCK_POSITION.CONTENT_BOTTOM] = {
+ top: DEFAULT_VALUES.navBarHeight + mediaAreaHeight - DROP_ZONE_DEFAUL_SIZE,
+ left: !isRTL ? sidebarSize : null,
+ right: isRTL ? sidebarSize : null,
+ width: mediaAreaWidth,
+ height: DROP_ZONE_DEFAUL_SIZE,
+ zIndex: cameraDockBounds.zIndex,
+ };
+
+ dropZones[CAMERADOCK_POSITION.CONTENT_LEFT] = {
+ top: DEFAULT_VALUES.navBarHeight + DROP_ZONE_DEFAUL_SIZE,
+ left: !isRTL ? sidebarSize : null,
+ right: isRTL ? sidebarSize : null,
+ height: mediaAreaHeight - 2 * DROP_ZONE_DEFAUL_SIZE,
+ width: DROP_ZONE_DEFAUL_SIZE,
+ zIndex: cameraDockBounds.zIndex,
+ };
+
+ dropZones[CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM] = {
+ top: windowHeight() - DROP_ZONE_DEFAUL_SIZE,
+ left: !isRTL ? sidebarNavWidth : null,
+ right: isRTL ? sidebarNavWidth : null,
+ width: sidebarContentWidth,
+ height: DROP_ZONE_DEFAUL_SIZE,
+ zIndex: cameraDockBounds.zIndex,
+ };
+
+ return dropZones;
+ };
+
+ const init = () => {
+ const { sidebarContentPanel } = sidebarContentInput;
+
+ if (isMobile) {
+ layoutContextDispatch({
+ type: ACTIONS.SET_LAYOUT_INPUT,
+ value: defaultsDeep(
+ {
+ sidebarNavigation: {
+ isOpen:
+ input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
+ sidebarNavPanel: sidebarNavigationInput.sidebarNavPanel,
+ },
+ sidebarContent: {
+ isOpen: sidebarContentPanel !== PANELS.NONE,
+ sidebarContentPanel: sidebarContentInput.sidebarContentPanel,
+ },
+ sidebarContentHorizontalResizer: {
+ isOpen: false,
+ },
+ presentation: {
+ isOpen: presentationInput.isOpen,
+ slidesLength: presentationInput.slidesLength,
+ currentSlide: {
+ ...presentationInput.currentSlide,
+ },
+ },
+ cameraDock: {
+ numCameras: cameraDockInput.numCameras,
+ },
+ externalVideo: {
+ hasExternalVideo: input.externalVideo.hasExternalVideo,
+ },
+ screenShare: {
+ hasScreenShare: input.screenShare.hasScreenShare,
+ width: input.screenShare.width,
+ height: input.screenShare.height,
+ },
+ sharedNotes: {
+ isPinned: sharedNotesInput.isPinned,
+ },
+ },
+ INITIAL_INPUT_STATE
+ ),
+ });
+ } else {
+ layoutContextDispatch({
+ type: ACTIONS.SET_LAYOUT_INPUT,
+ value: defaultsDeep(
+ {
+ sidebarNavigation: {
+ isOpen:
+ input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
+ },
+ sidebarContent: {
+ isOpen: sidebarContentPanel !== PANELS.NONE,
+ sidebarContentPanel,
+ },
+ sidebarContentHorizontalResizer: {
+ isOpen: false,
+ },
+ presentation: {
+ isOpen: presentationInput.isOpen,
+ slidesLength: presentationInput.slidesLength,
+ currentSlide: {
+ ...presentationInput.currentSlide,
+ },
+ },
+ cameraDock: {
+ numCameras: cameraDockInput.numCameras,
+ },
+ externalVideo: {
+ hasExternalVideo: input.externalVideo.hasExternalVideo,
+ },
+ screenShare: {
+ hasScreenShare: input.screenShare.hasScreenShare,
+ width: input.screenShare.width,
+ height: input.screenShare.height,
+ },
+ sharedNotes: {
+ isPinned: sharedNotesInput.isPinned,
+ },
+ },
+ INITIAL_INPUT_STATE
+ ),
+ });
+ }
+ Session.set('layoutReady', true);
+ throttledCalculatesLayout();
+ };
+
+ const calculatesSidebarContentHeight = (cameraDockHeight) => {
+ const { isOpen, slidesLength } = presentationInput;
+ const { hasExternalVideo } = externalVideoInput;
+ const { hasScreenShare } = screenShareInput;
+ const { isPinned: isSharedNotesPinned } = sharedNotesInput;
+
+ const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
+ const isGeneralMediaOff =
+ !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
+
+ let sidebarContentHeight = 0;
+ if (sidebarContentInput.isOpen) {
+ if (isMobile) {
+ sidebarContentHeight = windowHeight() - DEFAULT_VALUES.navBarHeight;
+ } else if (
+ cameraDockInput.numCameras > 0 &&
+ cameraDockInput.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM &&
+ isOpen &&
+ !isGeneralMediaOff
+ ) {
+ sidebarContentHeight = windowHeight() - cameraDockHeight;
+ } else {
+ sidebarContentHeight = windowHeight();
+ }
+ sidebarContentHeight -= bannerAreaHeight();
+ }
+ return sidebarContentHeight;
+ };
+
+ const calculatesCameraDockBounds = (sidebarNavWidth, sidebarContentWidth, mediaAreaBounds) => {
+ const { baseCameraDockBounds } = props;
+ const sidebarSize = sidebarNavWidth + sidebarContentWidth;
+
+ const baseBounds = baseCameraDockBounds(mediaAreaBounds, sidebarSize);
+
+ // do not proceed if using values from LayoutEngine
+ if (Object.keys(baseBounds).length > 0) {
+ return baseBounds;
+ }
+
+ const {
+ camerasMargin,
+ cameraDockMinHeight,
+ cameraDockMinWidth,
+ navBarHeight,
+ presentationToolbarMinWidth,
+ } = DEFAULT_VALUES;
+
+ const cameraDockBounds = {};
+
+ let cameraDockHeight = 0;
+ let cameraDockWidth = 0;
+
+ const lastSize = Storage.getItem('webcamSize') || { width: 0, height: 0 };
+ let { width: lastWidth, height: lastHeight } = lastSize;
+
+ if (cameraDockInput.isDragging) cameraDockBounds.zIndex = 99;
+ else cameraDockBounds.zIndex = 1;
+
+ const isCameraTop = cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_TOP;
+ const isCameraBottom = cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_BOTTOM;
+ const isCameraLeft = cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_LEFT;
+ const isCameraRight = cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_RIGHT;
+ const isCameraSidebar = cameraDockInput.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM;
+
+ const stoppedResizing = prevIsResizing && !isResizing;
+ if (stoppedResizing) {
+ const isCameraTopOrBottom =
+ cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_TOP ||
+ cameraDockInput.position === CAMERADOCK_POSITION.CONTENT_BOTTOM;
+
+ Storage.setItem('webcamSize', {
+ width: isCameraTopOrBottom || isCameraSidebar ? lastWidth : cameraDockInput.width,
+ height: isCameraTopOrBottom || isCameraSidebar ? cameraDockInput.height : lastHeight,
+ });
+
+ const updatedLastSize = Storage.getItem('webcamSize');
+ lastWidth = updatedLastSize.width;
+ lastHeight = updatedLastSize.height;
+ }
+
+ if (isCameraTop || isCameraBottom) {
+ if ((lastHeight === 0 && !isResizing) || (isCameraTop && isMobile)) {
+ cameraDockHeight = min(
+ max(mediaAreaBounds.height * 0.2, cameraDockMinHeight),
+ mediaAreaBounds.height - cameraDockMinHeight
+ );
+ } else {
+ const height = isResizing ? cameraDockInput.height : lastHeight;
+ cameraDockHeight = min(
+ max(height, cameraDockMinHeight),
+ mediaAreaBounds.height - cameraDockMinHeight
+ );
+ }
+
+ cameraDockBounds.top = navBarHeight;
+ cameraDockBounds.left = mediaAreaBounds.left;
+ cameraDockBounds.right = isRTL ? sidebarSize : null;
+ cameraDockBounds.minWidth = mediaAreaBounds.width;
+ cameraDockBounds.width = mediaAreaBounds.width;
+ cameraDockBounds.maxWidth = mediaAreaBounds.width;
+ cameraDockBounds.minHeight = cameraDockMinHeight;
+ cameraDockBounds.height = cameraDockHeight;
+ cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
+
+ if (isCameraBottom) {
+ cameraDockBounds.top += mediaAreaBounds.height - cameraDockHeight;
+ }
+
+ return cameraDockBounds;
+ }
+
+ if (isCameraLeft || isCameraRight) {
+ if (lastWidth === 0 && !isResizing) {
+ cameraDockWidth = min(
+ max(mediaAreaBounds.width * 0.2, cameraDockMinWidth),
+ mediaAreaBounds.width - cameraDockMinWidth
+ );
+ } else {
+ const width = isResizing ? cameraDockInput.width : lastWidth;
+ cameraDockWidth = min(
+ max(width, cameraDockMinWidth),
+ mediaAreaBounds.width - cameraDockMinWidth
+ );
+ }
+
+ cameraDockBounds.top = navBarHeight + bannerAreaHeight();
+ cameraDockBounds.minWidth = cameraDockMinWidth;
+ cameraDockBounds.width = cameraDockWidth;
+ cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
+ cameraDockBounds.presenterMaxWidth =
+ mediaAreaBounds.width - presentationToolbarMinWidth - camerasMargin;
+ cameraDockBounds.minHeight = cameraDockMinHeight;
+ cameraDockBounds.height = mediaAreaBounds.height;
+ cameraDockBounds.maxHeight = mediaAreaBounds.height;
+ // button size in vertical position
+ cameraDockBounds.height -= 20;
+
+ if (isCameraRight) {
+ const sizeValue = mediaAreaBounds.left + mediaAreaBounds.width - cameraDockWidth;
+ cameraDockBounds.left = !isRTL ? sizeValue - camerasMargin : 0;
+ cameraDockBounds.right = isRTL ? sizeValue + sidebarSize - camerasMargin : null;
+ } else if (isCameraLeft) {
+ cameraDockBounds.left = mediaAreaBounds.left + camerasMargin;
+ cameraDockBounds.right = isRTL ? sidebarSize + camerasMargin * 2 : null;
+ }
+
+ return cameraDockBounds;
+ }
+
+ if (isCameraSidebar) {
+ if (lastHeight === 0 && !isResizing) {
+ cameraDockHeight = min(
+ max(windowHeight() * 0.2, cameraDockMinHeight),
+ windowHeight() - cameraDockMinHeight
+ );
+ } else {
+ const height = isResizing ? cameraDockInput.height : lastHeight;
+ cameraDockHeight = min(
+ max(height, cameraDockMinHeight),
+ windowHeight() - cameraDockMinHeight
+ );
+ }
+
+ cameraDockBounds.top = windowHeight() - cameraDockHeight - bannerAreaHeight();
+ cameraDockBounds.left = !isRTL ? sidebarNavWidth : 0;
+ cameraDockBounds.right = isRTL ? sidebarNavWidth : 0;
+ cameraDockBounds.minWidth = sidebarContentWidth;
+ cameraDockBounds.width = sidebarContentWidth;
+ cameraDockBounds.maxWidth = sidebarContentWidth;
+ cameraDockBounds.minHeight = cameraDockMinHeight;
+ cameraDockBounds.height = cameraDockHeight;
+ cameraDockBounds.maxHeight = windowHeight() * 0.8;
+ }
+ return cameraDockBounds;
+ };
+
+ const calculatesMediaBounds = (sidebarNavWidth, sidebarContentWidth, cameraDockBounds) => {
+ const { isOpen, slidesLength } = presentationInput;
+ const { hasExternalVideo } = externalVideoInput;
+ const { hasScreenShare } = screenShareInput;
+ const { isPinned: isSharedNotesPinned } = sharedNotesInput;
+
+ const { height: actionBarHeight } = calculatesActionbarHeight();
+ const mediaAreaHeight =
+ windowHeight() - (DEFAULT_VALUES.navBarHeight + actionBarHeight + bannerAreaHeight());
+ const mediaAreaWidth = windowWidth() - (sidebarNavWidth + sidebarContentWidth);
+ const mediaBounds = {};
+ const { element: fullscreenElement } = fullscreen;
+ const { navBarHeight, camerasMargin } = DEFAULT_VALUES;
+
+ const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
+ const isGeneralMediaOff =
+ !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
+
+ if (!isOpen || isGeneralMediaOff) {
+ mediaBounds.width = 0;
+ mediaBounds.height = 0;
+ mediaBounds.top = 0;
+ mediaBounds.left = !isRTL ? 0 : null;
+ mediaBounds.right = isRTL ? 0 : null;
+ mediaBounds.zIndex = 0;
+ return mediaBounds;
+ }
+
+ if (
+ fullscreenElement === 'Presentation' ||
+ fullscreenElement === 'Screenshare' ||
+ fullscreenElement === 'ExternalVideo'
+ ) {
+ mediaBounds.width = windowWidth();
+ mediaBounds.height = windowHeight();
+ mediaBounds.top = 0;
+ mediaBounds.left = !isRTL ? 0 : null;
+ mediaBounds.right = isRTL ? 0 : null;
+ mediaBounds.zIndex = 99;
+ return mediaBounds;
+ }
+
+ const sidebarSize = sidebarNavWidth + sidebarContentWidth;
+
+ if (cameraDockInput.numCameras > 0 && !cameraDockInput.isDragging) {
+ switch (cameraDockInput.position) {
+ case CAMERADOCK_POSITION.CONTENT_TOP: {
+ mediaBounds.width = mediaAreaWidth;
+ mediaBounds.height = mediaAreaHeight - cameraDockBounds.height - camerasMargin;
+ mediaBounds.top =
+ navBarHeight + cameraDockBounds.height + camerasMargin + bannerAreaHeight();
+ mediaBounds.left = !isRTL ? sidebarSize : null;
+ mediaBounds.right = isRTL ? sidebarSize : null;
+ break;
+ }
+ case CAMERADOCK_POSITION.CONTENT_RIGHT: {
+ mediaBounds.width = mediaAreaWidth - cameraDockBounds.width - camerasMargin * 2;
+ mediaBounds.height = mediaAreaHeight;
+ mediaBounds.top = navBarHeight + bannerAreaHeight();
+ mediaBounds.left = !isRTL ? sidebarSize : null;
+ mediaBounds.right = isRTL ? sidebarSize - camerasMargin * 2 : null;
+ break;
+ }
+ case CAMERADOCK_POSITION.CONTENT_BOTTOM: {
+ mediaBounds.width = mediaAreaWidth;
+ mediaBounds.height = mediaAreaHeight - cameraDockBounds.height - camerasMargin;
+ mediaBounds.top = navBarHeight - camerasMargin + bannerAreaHeight();
+ mediaBounds.left = !isRTL ? sidebarSize : null;
+ mediaBounds.right = isRTL ? sidebarSize : null;
+ break;
+ }
+ case CAMERADOCK_POSITION.CONTENT_LEFT: {
+ mediaBounds.width = mediaAreaWidth - cameraDockBounds.width - camerasMargin * 2;
+ mediaBounds.height = mediaAreaHeight;
+ mediaBounds.top = navBarHeight + bannerAreaHeight();
+ const sizeValue =
+ sidebarNavWidth + sidebarContentWidth + mediaAreaWidth - mediaBounds.width;
+ mediaBounds.left = !isRTL ? sizeValue : null;
+ mediaBounds.right = isRTL ? sidebarSize : null;
+ break;
+ }
+ case CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM: {
+ mediaBounds.width = mediaAreaWidth;
+ mediaBounds.height = mediaAreaHeight;
+ mediaBounds.top = navBarHeight + bannerAreaHeight();
+ mediaBounds.left = !isRTL ? sidebarSize : null;
+ mediaBounds.right = isRTL ? sidebarSize : null;
+ break;
+ }
+ default: {
+ console.log('presentation - camera default');
+ }
+ }
+ mediaBounds.zIndex = 1;
+ } else {
+ mediaBounds.width = mediaAreaWidth;
+ mediaBounds.height = mediaAreaHeight;
+ mediaBounds.top = DEFAULT_VALUES.navBarHeight + bannerAreaHeight();
+ mediaBounds.left = !isRTL ? sidebarSize : null;
+ mediaBounds.right = isRTL ? sidebarSize : null;
+ }
+
+ return mediaBounds;
+ };
+
+ const calculatesLayout = () => {
+ const {
+ calculatesNavbarBounds,
+ calculatesActionbarBounds,
+ calculatesSidebarNavWidth,
+ calculatesSidebarNavHeight,
+ calculatesSidebarNavBounds,
+ calculatesSidebarContentWidth,
+ calculatesSidebarContentBounds,
+ calculatesMediaAreaBounds,
+ isTablet,
+ } = props;
+ const { position: cameraPosition } = cameraDockInput;
+ const { camerasMargin, captionsMargin } = DEFAULT_VALUES;
+
+ const sidebarNavWidth = calculatesSidebarNavWidth();
+ const sidebarNavHeight = calculatesSidebarNavHeight();
+ const sidebarContentWidth = calculatesSidebarContentWidth();
+ const sidebarNavBounds = calculatesSidebarNavBounds();
+ const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
+ const mediaAreaBounds = calculatesMediaAreaBounds(
+ sidebarNavWidth.width,
+ sidebarContentWidth.width
+ );
+ const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
+ const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
+ const cameraDockBounds = calculatesCameraDockBounds(
+ sidebarNavWidth.width,
+ sidebarContentWidth.width,
+ mediaAreaBounds
+ );
+ const dropZoneAreas = calculatesDropAreas(
+ sidebarNavWidth.width,
+ sidebarContentWidth.width,
+ cameraDockBounds
+ );
+ const sidebarContentHeight = calculatesSidebarContentHeight(cameraDockBounds.height);
+ const mediaBounds = calculatesMediaBounds(
+ sidebarNavWidth.width,
+ sidebarContentWidth.width,
+ cameraDockBounds
+ );
+ const sidebarSize = sidebarContentWidth.width + sidebarNavWidth.width;
+ const { height: actionBarHeight } = calculatesActionbarHeight();
+
+ let horizontalCameraDiff = 0;
+
+ if (cameraPosition === CAMERADOCK_POSITION.CONTENT_LEFT) {
+ horizontalCameraDiff = cameraDockBounds.width + camerasMargin * 2;
+ }
+
+ if (cameraPosition === CAMERADOCK_POSITION.CONTENT_RIGHT) {
+ horizontalCameraDiff = camerasMargin * 2;
+ }
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_NAVBAR_OUTPUT,
+ value: {
+ display: navbarInput.hasNavBar,
+ width: navbarBounds.width,
+ height: navbarBounds.height,
+ top: navbarBounds.top,
+ left: navbarBounds.left,
+ tabOrder: DEFAULT_VALUES.navBarTabOrder,
+ zIndex: navbarBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_ACTIONBAR_OUTPUT,
+ value: {
+ display: actionbarInput.hasActionBar,
+ width: actionbarBounds.width,
+ height: actionbarBounds.height,
+ innerHeight: actionbarBounds.innerHeight,
+ top: actionbarBounds.top,
+ left: actionbarBounds.left,
+ padding: actionbarBounds.padding,
+ tabOrder: DEFAULT_VALUES.actionBarTabOrder,
+ zIndex: actionbarBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAPTIONS_OUTPUT,
+ value: {
+ left: !isRTL ? sidebarSize + captionsMargin : null,
+ right: isRTL ? sidebarSize + captionsMargin : null,
+ maxWidth: mediaAreaBounds.width - captionsMargin * 2,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_NAVIGATION_OUTPUT,
+ value: {
+ display: sidebarNavigationInput.isOpen,
+ minWidth: sidebarNavWidth.minWidth,
+ width: sidebarNavWidth.width,
+ maxWidth: sidebarNavWidth.maxWidth,
+ height: sidebarNavHeight,
+ top: sidebarNavBounds.top,
+ left: sidebarNavBounds.left,
+ right: sidebarNavBounds.right,
+ tabOrder: DEFAULT_VALUES.sidebarNavTabOrder,
+ isResizable: !isMobile && !isTablet,
+ zIndex: sidebarNavBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_NAVIGATION_RESIZABLE_EDGE,
+ value: {
+ top: false,
+ right: !isRTL,
+ bottom: false,
+ left: isRTL,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_OUTPUT,
+ value: {
+ display: sidebarContentInput.isOpen,
+ minWidth: sidebarContentWidth.minWidth,
+ width: sidebarContentWidth.width,
+ maxWidth: sidebarContentWidth.maxWidth,
+ height: sidebarContentHeight,
+ top: sidebarContentBounds.top,
+ left: sidebarContentBounds.left,
+ right: sidebarContentBounds.right,
+ currentPanelType,
+ tabOrder: DEFAULT_VALUES.sidebarContentTabOrder,
+ isResizable: !isMobile && !isTablet,
+ zIndex: sidebarContentBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_RESIZABLE_EDGE,
+ value: {
+ top: false,
+ right: !isRTL,
+ bottom: false,
+ left: isRTL,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_MEDIA_AREA_SIZE,
+ value: {
+ width: windowWidth() - sidebarNavWidth.width - sidebarContentWidth.width,
+ height: windowHeight() - DEFAULT_VALUES.navBarHeight - actionBarHeight,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAMERA_DOCK_OUTPUT,
+ value: {
+ display: cameraDockInput.numCameras > 0,
+ position: cameraDockInput.position,
+ minWidth: cameraDockBounds.minWidth,
+ width: cameraDockBounds.width,
+ maxWidth: cameraDockBounds.maxWidth,
+ presenterMaxWidth: cameraDockBounds.presenterMaxWidth,
+ minHeight: cameraDockBounds.minHeight,
+ height: cameraDockBounds.height,
+ maxHeight: cameraDockBounds.maxHeight,
+ top: cameraDockBounds.top,
+ left: cameraDockBounds.left,
+ right: cameraDockBounds.right,
+ tabOrder: 4,
+ isDraggable: !isMobile && !isTablet && presentationInput.isOpen,
+ resizableEdge: {
+ top:
+ input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_BOTTOM ||
+ (input.cameraDock.position === CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM &&
+ input.sidebarContent.isOpen),
+ right:
+ (!isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT) ||
+ (isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT),
+ bottom: input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_TOP,
+ left:
+ (!isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_RIGHT) ||
+ (isRTL && input.cameraDock.position === CAMERADOCK_POSITION.CONTENT_LEFT),
+ },
+ zIndex: cameraDockBounds.zIndex,
+ focusedId: input.cameraDock.focusedId,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_DROP_AREAS,
+ value: dropZoneAreas,
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_PRESENTATION_OUTPUT,
+ value: {
+ display: presentationInput.isOpen,
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
+ tabOrder: DEFAULT_VALUES.presentationTabOrder,
+ isResizable: false,
+ zIndex: mediaBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SCREEN_SHARE_OUTPUT,
+ value: {
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
+ zIndex: mediaBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_EXTERNAL_VIDEO_OUTPUT,
+ value: {
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
+ value: {
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
+ },
+ });
+ };
+
+ return null;
+};
+
+export default CustomLayout;
\ No newline at end of file
diff --git a/src/2.7.12/imports/ui/components/layout/layout-manager/layoutEngine.jsx b/src/2.7.12/imports/ui/components/layout/layout-manager/layoutEngine.jsx
new file mode 100644
index 00000000..a111f242
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/layout-manager/layoutEngine.jsx
@@ -0,0 +1,319 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { layoutSelect, layoutSelectInput } from '/imports/ui/components/layout/context';
+import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
+import { LAYOUT_TYPE, DEVICE_TYPE } from '/imports/ui/components/layout/enums';
+
+import CustomLayout from '/imports/ui/components/layout/layout-manager/customLayout';
+import SmartLayout from '/imports/ui/components/layout/layout-manager/smartLayout';
+import PresentationFocusLayout from '/imports/ui/components/layout/layout-manager/presentationFocusLayout';
+import VideoFocusLayout from '/imports/ui/components/layout/layout-manager/videoFocusLayout';
+import { isPresentationEnabled } from '/imports/ui/services/features';
+
+const propTypes = {
+ layoutType: PropTypes.string.isRequired,
+};
+
+const LayoutEngine = ({ layoutType }) => {
+ const bannerBarInput = layoutSelectInput((i) => i.bannerBar);
+ const notificationsBarInput = layoutSelectInput((i) => i.notificationsBar);
+ const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
+ const presentationInput = layoutSelectInput((i) => i.presentation);
+ const actionbarInput = layoutSelectInput((i) => i.actionBar);
+ const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
+ const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
+ const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
+ const screenShareInput = layoutSelectInput((i) => i.screenShare);
+ const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
+
+ const fullscreen = layoutSelect((i) => i.fullscreen);
+ const isRTL = layoutSelect((i) => i.isRTL);
+ const fontSize = layoutSelect((i) => i.fontSize);
+ const deviceType = layoutSelect((i) => i.deviceType);
+
+ const isMobile = deviceType === DEVICE_TYPE.MOBILE;
+ const isTablet = deviceType === DEVICE_TYPE.TABLET;
+ const windowWidth = () => window.document.documentElement.clientWidth;
+ const windowHeight = () => window.document.documentElement.clientHeight;
+ const min = (value1, value2) => (value1 <= value2 ? value1 : value2);
+ const max = (value1, value2) => (value1 >= value2 ? value1 : value2);
+
+ const bannerAreaHeight = () => {
+ const { hasNotification } = notificationsBarInput;
+ const { hasBanner } = bannerBarInput;
+ const bannerHeight = hasBanner ? DEFAULT_VALUES.bannerHeight : 0;
+ const notificationHeight = hasNotification ? DEFAULT_VALUES.bannerHeight : 0;
+
+ return bannerHeight + notificationHeight;
+ };
+
+ const baseCameraDockBounds = (mediaAreaBounds, sidebarSize) => {
+ const { isOpen, slidesLength } = presentationInput;
+ const { hasExternalVideo } = externalVideoInput;
+ const { hasScreenShare } = screenShareInput;
+ const { isPinned: isSharedNotesPinned } = sharedNotesInput;
+
+ const cameraDockBounds = {};
+
+ if (cameraDockInput.numCameras === 0 && layoutType !== LAYOUT_TYPE.VIDEO_FOCUS) {
+ cameraDockBounds.width = 0;
+ cameraDockBounds.height = 0;
+
+ return cameraDockBounds;
+ }
+
+ const hasPresentation = isPresentationEnabled() && slidesLength !== 0
+ const isGeneralMediaOff = !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
+
+ if (!isOpen || isGeneralMediaOff) {
+ cameraDockBounds.width = mediaAreaBounds.width;
+ cameraDockBounds.maxWidth = mediaAreaBounds.width;
+ cameraDockBounds.height = mediaAreaBounds.height - bannerAreaHeight();
+ cameraDockBounds.maxHeight = mediaAreaBounds.height;
+ cameraDockBounds.top = DEFAULT_VALUES.navBarHeight + bannerAreaHeight();
+ cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : 0;
+ cameraDockBounds.right = isRTL ? sidebarSize : null;
+ }
+
+ if (fullscreen.group === 'webcams') {
+ cameraDockBounds.width = windowWidth();
+ cameraDockBounds.minWidth = windowWidth();
+ cameraDockBounds.maxWidth = windowWidth();
+ cameraDockBounds.height = windowHeight();
+ cameraDockBounds.minHeight = windowHeight();
+ cameraDockBounds.maxHeight = windowHeight();
+ cameraDockBounds.top = 0;
+ cameraDockBounds.left = 0;
+ cameraDockBounds.right = 0;
+ cameraDockBounds.zIndex = 99;
+
+ return cameraDockBounds;
+ }
+
+ return cameraDockBounds;
+ };
+
+ const calculatesNavbarBounds = (mediaAreaBounds) => {
+ const { navBarHeight, navBarTop } = DEFAULT_VALUES;
+
+ return {
+ width: mediaAreaBounds.width,
+ height: navBarHeight,
+ top: navBarTop + bannerAreaHeight(),
+ left: !isRTL ? mediaAreaBounds.left : 0,
+ zIndex: 1,
+ };
+ };
+
+ const calculatesActionbarHeight = () => {
+ const { actionBarHeight, actionBarPadding } = DEFAULT_VALUES;
+
+ const BASE_FONT_SIZE = 14; // 90% font size
+ const height = ((actionBarHeight / BASE_FONT_SIZE) * fontSize);
+
+ return {
+ height: height + (actionBarPadding * 2),
+ innerHeight: height,
+ padding: actionBarPadding,
+ };
+ };
+
+ const calculatesActionbarBounds = (mediaAreaBounds) => {
+ const actionBarHeight = calculatesActionbarHeight();
+
+ return {
+ display: actionbarInput.hasActionBar,
+ width: mediaAreaBounds.width,
+ height: actionBarHeight.height,
+ innerHeight: actionBarHeight.innerHeight,
+ padding: actionBarHeight.padding,
+ top: windowHeight() - actionBarHeight.height,
+ left: !isRTL ? mediaAreaBounds.left : 0,
+ zIndex: 1,
+ };
+ };
+
+ const calculatesSidebarNavWidth = () => {
+ const {
+ sidebarNavMinWidth,
+ sidebarNavMaxWidth,
+ } = DEFAULT_VALUES;
+
+ const { isOpen, width: sidebarNavWidth } = sidebarNavigationInput;
+
+ let minWidth = 0;
+ let width = 0;
+ let maxWidth = 0;
+ if (isOpen) {
+ if (isMobile) {
+ minWidth = windowWidth();
+ width = windowWidth();
+ maxWidth = windowWidth();
+ } else {
+ if (sidebarNavWidth === 0) {
+ width = min(max((windowWidth() * 0.2), sidebarNavMinWidth), sidebarNavMaxWidth);
+ } else {
+ width = min(max(sidebarNavWidth, sidebarNavMinWidth), sidebarNavMaxWidth);
+ }
+ minWidth = sidebarNavMinWidth;
+ maxWidth = sidebarNavMaxWidth;
+ }
+ }
+ return {
+ minWidth,
+ width,
+ maxWidth,
+ };
+ };
+
+ const calculatesSidebarNavHeight = () => {
+ const { navBarHeight } = DEFAULT_VALUES;
+ const { isOpen } = sidebarNavigationInput;
+
+ let sidebarNavHeight = 0;
+ if (isOpen) {
+ if (isMobile) {
+ sidebarNavHeight = windowHeight() - navBarHeight - bannerAreaHeight();
+ } else {
+ sidebarNavHeight = windowHeight() - bannerAreaHeight();
+ }
+ }
+ return sidebarNavHeight;
+ };
+
+ const calculatesSidebarNavBounds = () => {
+ const { sidebarNavTop, navBarHeight, sidebarNavLeft } = DEFAULT_VALUES;
+
+ let top = sidebarNavTop + bannerAreaHeight();
+
+ if (isMobile) {
+ top = navBarHeight + bannerAreaHeight();
+ }
+
+ return {
+ top,
+ left: !isRTL ? sidebarNavLeft : null,
+ right: isRTL ? sidebarNavLeft : null,
+ zIndex: isMobile ? 11 : 2,
+ };
+ };
+
+ const calculatesSidebarContentWidth = () => {
+ const {
+ sidebarContentMinWidth,
+ sidebarContentMaxWidth,
+ } = DEFAULT_VALUES;
+
+ const { isOpen, width: sidebarContentWidth } = sidebarContentInput;
+
+ let minWidth = 0;
+ let width = 0;
+ let maxWidth = 0;
+
+ if (isOpen) {
+ if (isMobile) {
+ minWidth = windowWidth();
+ width = windowWidth();
+ maxWidth = windowWidth();
+ } else {
+ if (sidebarContentWidth === 0) {
+ width = min(
+ max((windowWidth() * 0.2), sidebarContentMinWidth), sidebarContentMaxWidth,
+ );
+ } else {
+ width = min(max(sidebarContentWidth, sidebarContentMinWidth),
+ sidebarContentMaxWidth);
+ }
+ minWidth = sidebarContentMinWidth;
+ maxWidth = sidebarContentMaxWidth;
+ }
+ }
+ return {
+ minWidth,
+ width,
+ maxWidth,
+ };
+ };
+
+ const calculatesSidebarContentBounds = (sidebarNavWidth) => {
+ const { navBarHeight, sidebarNavTop } = DEFAULT_VALUES;
+
+ let top = sidebarNavTop + bannerAreaHeight();
+
+ if (isMobile) top = navBarHeight + bannerAreaHeight();
+
+ let left = isMobile ? 0 : sidebarNavWidth;
+ let right = isMobile ? 0 : sidebarNavWidth;
+ left = !isRTL ? left : null;
+ right = isRTL ? right : null;
+
+ const zIndex = isMobile ? 11 : 1;
+
+ return {
+ top,
+ left,
+ right,
+ zIndex,
+ };
+ };
+
+ const calculatesMediaAreaBounds = (sidebarNavWidth, sidebarContentWidth) => {
+ const { navBarHeight } = DEFAULT_VALUES;
+ const { height: actionBarHeight } = calculatesActionbarHeight();
+ let left = 0;
+ let width = 0;
+ if (isMobile) {
+ width = windowWidth();
+ } else {
+ left = !isRTL ? sidebarNavWidth + sidebarContentWidth : 0;
+ width = windowWidth() - sidebarNavWidth - sidebarContentWidth;
+ }
+
+ return {
+ width,
+ height: windowHeight() - (navBarHeight + actionBarHeight + bannerAreaHeight()),
+ top: navBarHeight + bannerAreaHeight(),
+ left,
+ };
+ };
+
+ const common = {
+ bannerAreaHeight,
+ baseCameraDockBounds,
+ calculatesNavbarBounds,
+ calculatesActionbarHeight,
+ calculatesActionbarBounds,
+ calculatesSidebarNavWidth,
+ calculatesSidebarNavHeight,
+ calculatesSidebarNavBounds,
+ calculatesSidebarContentWidth,
+ calculatesSidebarContentBounds,
+ calculatesMediaAreaBounds,
+ isMobile,
+ isTablet,
+ };
+
+ const layout = document.getElementById('layout');
+
+ switch (layoutType) {
+ case LAYOUT_TYPE.CUSTOM_LAYOUT:
+ layout?.setAttribute("data-layout", LAYOUT_TYPE.CUSTOM_LAYOUT);
+ return ;
+ case LAYOUT_TYPE.SMART_LAYOUT:
+ layout?.setAttribute("data-layout", LAYOUT_TYPE.SMART_LAYOUT);
+ return ;
+ case LAYOUT_TYPE.PRESENTATION_FOCUS:
+ layout?.setAttribute("data-layout", LAYOUT_TYPE.PRESENTATION_FOCUS);
+ return ;
+ case LAYOUT_TYPE.VIDEO_FOCUS:
+ layout?.setAttribute("data-layout",LAYOUT_TYPE.VIDEO_FOCUS);
+ return ;
+ default:
+ layout?.setAttribute("data-layout", LAYOUT_TYPE.CUSTOM_LAYOUT);
+ return ;
+ }
+};
+
+LayoutEngine.propTypes = propTypes;
+
+export default LayoutEngine;
diff --git a/src/2.7.12/imports/ui/components/layout/layout-manager/presentationFocusLayout.jsx b/src/2.7.12/imports/ui/components/layout/layout-manager/presentationFocusLayout.jsx
new file mode 100644
index 00000000..b37d0761
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/layout-manager/presentationFocusLayout.jsx
@@ -0,0 +1,514 @@
+import { useEffect, useRef } from 'react';
+import { throttle } from '/imports/utils/throttle';
+import { layoutDispatch, layoutSelect, layoutSelectInput } from '/imports/ui/components/layout/context';
+import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
+import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
+import {
+ ACTIONS,
+ PANELS,
+ CAMERADOCK_POSITION,
+} from '/imports/ui/components/layout/enums';
+import { defaultsDeep } from '/imports/utils/array-utils';
+import { isPresentationEnabled } from '/imports/ui/services/features';
+
+const windowWidth = () => window.document.documentElement.clientWidth;
+const windowHeight = () => window.document.documentElement.clientHeight;
+const min = (value1, value2) => (value1 <= value2 ? value1 : value2);
+const max = (value1, value2) => (value1 >= value2 ? value1 : value2);
+
+const PresentationFocusLayout = (props) => {
+ const { bannerAreaHeight, isMobile } = props;
+
+ function usePrevious(value) {
+ const ref = useRef();
+ useEffect(() => {
+ ref.current = value;
+ });
+ return ref.current;
+ }
+
+ const input = layoutSelect((i) => i.input);
+ const deviceType = layoutSelect((i) => i.deviceType);
+ const isRTL = layoutSelect((i) => i.isRTL);
+ const fullscreen = layoutSelect((i) => i.fullscreen);
+ const fontSize = layoutSelect((i) => i.fontSize);
+ const currentPanelType = layoutSelect((i) => i.currentPanelType);
+
+ const presentationInput = layoutSelectInput((i) => i.presentation);
+ const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
+ const screenShareInput = layoutSelectInput((i) => i.screenShare);
+ const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
+
+ const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
+ const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
+ const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
+ const actionbarInput = layoutSelectInput((i) => i.actionBar);
+ const navbarInput = layoutSelectInput((i) => i.navBar);
+ const layoutContextDispatch = layoutDispatch();
+
+ const prevDeviceType = usePrevious(deviceType);
+
+ const throttledCalculatesLayout = throttle(() => calculatesLayout(),
+ 50, { trailing: true, leading: true });
+
+ useEffect(() => {
+ window.addEventListener('resize', () => {
+ layoutContextDispatch({
+ type: ACTIONS.SET_BROWSER_SIZE,
+ value: {
+ width: window.document.documentElement.clientWidth,
+ height: window.document.documentElement.clientHeight,
+ },
+ });
+ });
+ }, []);
+
+ useEffect(() => {
+ if (deviceType === null) return () => null;
+
+ if (deviceType !== prevDeviceType) {
+ // reset layout if deviceType changed
+ // not all options is supported in all devices
+ init();
+ } else {
+ throttledCalculatesLayout();
+ }
+ }, [input, deviceType, isRTL, fontSize, fullscreen]);
+
+ const init = () => {
+ const { sidebarContentPanel } = sidebarContentInput;
+ if (isMobile) {
+ layoutContextDispatch({
+ type: ACTIONS.SET_LAYOUT_INPUT,
+ value: defaultsDeep(
+ {
+ sidebarNavigation: {
+ isOpen:
+ input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
+ },
+ sidebarContent: {
+ isOpen: sidebarContentPanel !== PANELS.NONE,
+ sidebarContentPanel,
+ },
+ SidebarContentHorizontalResizer: {
+ isOpen: false,
+ },
+ presentation: {
+ isOpen: presentationInput.isOpen,
+ slidesLength: presentationInput.slidesLength,
+ currentSlide: {
+ ...presentationInput.currentSlide,
+ },
+ },
+ cameraDock: {
+ numCameras: cameraDockInput.numCameras,
+ },
+ externalVideo: {
+ hasExternalVideo: input.externalVideo.hasExternalVideo,
+ },
+ screenShare: {
+ hasScreenShare: input.screenShare.hasScreenShare,
+ width: input.screenShare.width,
+ height: input.screenShare.height,
+ },
+ },
+ INITIAL_INPUT_STATE
+ ),
+ });
+ } else {
+ layoutContextDispatch({
+ type: ACTIONS.SET_LAYOUT_INPUT,
+ value: defaultsDeep(
+ {
+ sidebarNavigation: {
+ isOpen:
+ input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
+ },
+ sidebarContent: {
+ isOpen: sidebarContentPanel !== PANELS.NONE,
+ sidebarContentPanel,
+ },
+ SidebarContentHorizontalResizer: {
+ isOpen: false,
+ },
+ presentation: {
+ isOpen: presentationInput.isOpen,
+ slidesLength: presentationInput.slidesLength,
+ currentSlide: {
+ ...presentationInput.currentSlide,
+ },
+ },
+ cameraDock: {
+ numCameras: cameraDockInput.numCameras,
+ },
+ externalVideo: {
+ hasExternalVideo: input.externalVideo.hasExternalVideo,
+ },
+ screenShare: {
+ hasScreenShare: input.screenShare.hasScreenShare,
+ width: input.screenShare.width,
+ height: input.screenShare.height,
+ },
+ },
+ INITIAL_INPUT_STATE
+ ),
+ });
+ }
+ Session.set('layoutReady', true);
+ throttledCalculatesLayout();
+ };
+
+ const calculatesSidebarContentHeight = () => {
+ const { isOpen, slidesLength } = presentationInput;
+ const { hasExternalVideo } = externalVideoInput;
+ const { hasScreenShare } = screenShareInput;
+ const { isPinned: isSharedNotesPinned } = sharedNotesInput;
+
+ const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
+ const isGeneralMediaOff =
+ !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
+
+ const { navBarHeight, sidebarContentMinHeight } = DEFAULT_VALUES;
+ let height = 0;
+ let minHeight = 0;
+ let maxHeight = 0;
+ if (sidebarContentInput.isOpen) {
+ if (isMobile) {
+ height = windowHeight() - navBarHeight - bannerAreaHeight();
+ minHeight = height;
+ maxHeight = height;
+ } else if (cameraDockInput.numCameras > 0 && isOpen && !isGeneralMediaOff) {
+ if (sidebarContentInput.height === 0) {
+ height = windowHeight() * 0.75 - bannerAreaHeight();
+ } else {
+ height = min(max(sidebarContentInput.height, sidebarContentMinHeight), windowHeight());
+ }
+ minHeight = windowHeight() * 0.25 - bannerAreaHeight();
+ maxHeight = windowHeight() * 0.75 - bannerAreaHeight();
+ } else {
+ height = windowHeight() - bannerAreaHeight();
+ minHeight = height;
+ maxHeight = height;
+ }
+ }
+ return {
+ height,
+ minHeight,
+ maxHeight,
+ };
+ };
+
+ const calculatesCameraDockBounds = (
+ mediaBounds,
+ mediaAreaBounds,
+ sidebarNavWidth,
+ sidebarContentWidth,
+ sidebarContentHeight
+ ) => {
+ const { baseCameraDockBounds } = props;
+ const sidebarSize = sidebarNavWidth + sidebarContentWidth;
+
+ const baseBounds = baseCameraDockBounds(mediaAreaBounds, sidebarSize);
+
+ // do not proceed if using values from LayoutEngine
+ if (Object.keys(baseBounds).length > 0) {
+ return baseBounds;
+ }
+
+ const { cameraDockMinHeight } = DEFAULT_VALUES;
+
+ const cameraDockBounds = {};
+
+ let cameraDockHeight = 0;
+
+ if (isMobile) {
+ cameraDockBounds.top = mediaAreaBounds.top + mediaBounds.height;
+ cameraDockBounds.left = 0;
+ cameraDockBounds.right = 0;
+ cameraDockBounds.minWidth = mediaAreaBounds.width;
+ cameraDockBounds.width = mediaAreaBounds.width;
+ cameraDockBounds.maxWidth = mediaAreaBounds.width;
+ cameraDockBounds.minHeight = cameraDockMinHeight;
+ cameraDockBounds.height = mediaAreaBounds.height - mediaBounds.height;
+ cameraDockBounds.maxHeight = mediaAreaBounds.height - mediaBounds.height;
+ } else {
+ if (cameraDockInput.height === 0) {
+ cameraDockHeight = min(
+ max(windowHeight() - sidebarContentHeight, cameraDockMinHeight),
+ windowHeight() - cameraDockMinHeight
+ );
+ const bannerAreaDiff =
+ windowHeight() - sidebarContentHeight - cameraDockHeight - bannerAreaHeight();
+ cameraDockHeight += bannerAreaDiff;
+ } else {
+ cameraDockHeight = min(
+ max(cameraDockInput.height, cameraDockMinHeight),
+ windowHeight() - cameraDockMinHeight
+ );
+ }
+ cameraDockBounds.top = windowHeight() - cameraDockHeight - bannerAreaHeight();
+ cameraDockBounds.left = !isRTL ? sidebarNavWidth : 0;
+ cameraDockBounds.right = isRTL ? sidebarNavWidth : 0;
+ cameraDockBounds.minWidth = sidebarContentWidth;
+ cameraDockBounds.width = sidebarContentWidth;
+ cameraDockBounds.maxWidth = sidebarContentWidth;
+ cameraDockBounds.minHeight = cameraDockMinHeight;
+ cameraDockBounds.height = cameraDockHeight;
+ cameraDockBounds.maxHeight = windowHeight() - sidebarContentHeight;
+ cameraDockBounds.zIndex = 1;
+ }
+ return cameraDockBounds;
+ };
+
+ const calculatesMediaBounds = (mediaAreaBounds, sidebarSize) => {
+ const mediaBounds = {};
+ const { element: fullscreenElement } = fullscreen;
+
+ if (
+ fullscreenElement === 'Presentation' ||
+ fullscreenElement === 'Screenshare' ||
+ fullscreenElement === 'ExternalVideo'
+ ) {
+ mediaBounds.width = windowWidth();
+ mediaBounds.height = windowHeight();
+ mediaBounds.top = 0;
+ mediaBounds.left = !isRTL ? 0 : null;
+ mediaBounds.right = isRTL ? 0 : null;
+ mediaBounds.zIndex = 99;
+ return mediaBounds;
+ }
+
+ if (isMobile && cameraDockInput.numCameras > 0) {
+ mediaBounds.height = mediaAreaBounds.height * 0.7;
+ } else {
+ mediaBounds.height = mediaAreaBounds.height;
+ }
+ mediaBounds.width = mediaAreaBounds.width;
+ mediaBounds.top = DEFAULT_VALUES.navBarHeight + bannerAreaHeight();
+ mediaBounds.left = !isRTL ? mediaAreaBounds.left : null;
+ mediaBounds.right = isRTL ? sidebarSize : null;
+ mediaBounds.zIndex = 1;
+
+ return mediaBounds;
+ };
+
+ const calculatesLayout = () => {
+ const {
+ calculatesNavbarBounds,
+ calculatesActionbarBounds,
+ calculatesSidebarNavWidth,
+ calculatesSidebarNavHeight,
+ calculatesSidebarNavBounds,
+ calculatesSidebarContentWidth,
+ calculatesSidebarContentBounds,
+ calculatesMediaAreaBounds,
+ isTablet,
+ } = props;
+ const { captionsMargin } = DEFAULT_VALUES;
+
+ const sidebarNavWidth = calculatesSidebarNavWidth();
+ const sidebarNavHeight = calculatesSidebarNavHeight();
+ const sidebarContentWidth = calculatesSidebarContentWidth();
+ const sidebarNavBounds = calculatesSidebarNavBounds();
+ const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
+ const mediaAreaBounds = calculatesMediaAreaBounds(
+ sidebarNavWidth.width,
+ sidebarContentWidth.width
+ );
+ const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
+ const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
+ const sidebarSize = sidebarContentWidth.width + sidebarNavWidth.width;
+ const mediaBounds = calculatesMediaBounds(mediaAreaBounds, sidebarSize);
+ const sidebarContentHeight = calculatesSidebarContentHeight();
+ const cameraDockBounds = calculatesCameraDockBounds(
+ mediaBounds,
+ mediaAreaBounds,
+ sidebarNavWidth.width,
+ sidebarContentWidth.width,
+ sidebarContentHeight.height
+ );
+ const { isOpen } = presentationInput;
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_NAVBAR_OUTPUT,
+ value: {
+ display: navbarInput.hasNavBar,
+ width: navbarBounds.width,
+ height: navbarBounds.height,
+ top: navbarBounds.top,
+ left: navbarBounds.left,
+ tabOrder: DEFAULT_VALUES.navBarTabOrder,
+ zIndex: navbarBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_ACTIONBAR_OUTPUT,
+ value: {
+ display: actionbarInput.hasActionBar,
+ width: actionbarBounds.width,
+ height: actionbarBounds.height,
+ innerHeight: actionbarBounds.innerHeight,
+ top: actionbarBounds.top,
+ left: actionbarBounds.left,
+ padding: actionbarBounds.padding,
+ tabOrder: DEFAULT_VALUES.actionBarTabOrder,
+ zIndex: actionbarBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAPTIONS_OUTPUT,
+ value: {
+ left: !isRTL ? sidebarSize + captionsMargin : null,
+ right: isRTL ? sidebarSize + captionsMargin : null,
+ maxWidth: mediaAreaBounds.width - captionsMargin * 2,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_NAVIGATION_OUTPUT,
+ value: {
+ display: sidebarNavigationInput.isOpen,
+ minWidth: sidebarNavWidth.minWidth,
+ width: sidebarNavWidth.width,
+ maxWidth: sidebarNavWidth.maxWidth,
+ height: sidebarNavHeight,
+ top: sidebarNavBounds.top,
+ left: sidebarNavBounds.left,
+ right: sidebarNavBounds.right,
+ tabOrder: DEFAULT_VALUES.sidebarNavTabOrder,
+ isResizable: !isMobile && !isTablet,
+ zIndex: sidebarNavBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_NAVIGATION_RESIZABLE_EDGE,
+ value: {
+ top: false,
+ right: !isRTL,
+ bottom: false,
+ left: isRTL,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_OUTPUT,
+ value: {
+ display: sidebarContentInput.isOpen,
+ minWidth: sidebarContentWidth.minWidth,
+ width: sidebarContentWidth.width,
+ maxWidth: sidebarContentWidth.maxWidth,
+ minHeight: sidebarContentHeight.minHeight,
+ height: sidebarContentHeight.height,
+ maxHeight: sidebarContentHeight.maxHeight,
+ top: sidebarContentBounds.top,
+ left: sidebarContentBounds.left,
+ right: sidebarContentBounds.right,
+ currentPanelType,
+ tabOrder: DEFAULT_VALUES.sidebarContentTabOrder,
+ isResizable: !isMobile && !isTablet,
+ zIndex: sidebarContentBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_RESIZABLE_EDGE,
+ value: {
+ top: false,
+ right: !isRTL,
+ bottom: cameraDockInput.numCameras > 0,
+ left: isRTL,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_MEDIA_AREA_SIZE,
+ value: {
+ width: mediaAreaBounds.width,
+ height: mediaAreaBounds.height,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAMERA_DOCK_OUTPUT,
+ value: {
+ display: cameraDockInput.numCameras > 0,
+ position: CAMERADOCK_POSITION.SIDEBAR_CONTENT_BOTTOM,
+ minWidth: cameraDockBounds.minWidth,
+ width: cameraDockBounds.width,
+ maxWidth: cameraDockBounds.maxWidth,
+ minHeight: cameraDockBounds.minHeight,
+ height: cameraDockBounds.height,
+ maxHeight: cameraDockBounds.maxHeight,
+ top: cameraDockBounds.top,
+ left: cameraDockBounds.left,
+ right: cameraDockBounds.right,
+ tabOrder: 4,
+ isDraggable: false,
+ resizableEdge: {
+ top: false,
+ right: false,
+ bottom: false,
+ left: false,
+ },
+ focusedId: input.cameraDock.focusedId,
+ zIndex: cameraDockBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_PRESENTATION_OUTPUT,
+ value: {
+ display: presentationInput.isOpen,
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right : null,
+ tabOrder: DEFAULT_VALUES.presentationTabOrder,
+ isResizable: false,
+ zIndex: mediaBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SCREEN_SHARE_OUTPUT,
+ value: {
+ width: mediaBounds.width,
+ height: isOpen ? mediaBounds.height : 0,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: mediaBounds.right,
+ zIndex: mediaBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_EXTERNAL_VIDEO_OUTPUT,
+ value: {
+ width: isOpen ? mediaBounds.width : 0,
+ height: isOpen ? mediaBounds.height : 0,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: mediaBounds.right,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
+ value: {
+ width: isOpen ? mediaBounds.width : 0,
+ height: isOpen ? mediaBounds.height : 0,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: mediaBounds.right,
+ },
+ });
+ };
+
+ return null;
+};
+
+export default PresentationFocusLayout;
\ No newline at end of file
diff --git a/src/2.7.12/imports/ui/components/layout/layout-manager/smartLayout.jsx b/src/2.7.12/imports/ui/components/layout/layout-manager/smartLayout.jsx
new file mode 100644
index 00000000..a10756e2
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/layout-manager/smartLayout.jsx
@@ -0,0 +1,585 @@
+import { useEffect, useRef } from 'react';
+import { throttle } from '/imports/utils/throttle';
+import { layoutDispatch, layoutSelect, layoutSelectInput } from '/imports/ui/components/layout/context';
+import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
+import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
+import { ACTIONS, PANELS, CAMERADOCK_POSITION } from '/imports/ui/components/layout/enums';
+import { defaultsDeep } from '/imports/utils/array-utils';
+import { isPresentationEnabled } from '/imports/ui/services/features';
+
+const windowWidth = () => window.document.documentElement.clientWidth;
+const windowHeight = () => window.document.documentElement.clientHeight;
+
+const SmartLayout = (props) => {
+ const { bannerAreaHeight, isMobile } = props;
+
+ function usePrevious(value) {
+ const ref = useRef();
+ useEffect(() => {
+ ref.current = value;
+ });
+ return ref.current;
+ }
+
+ const input = layoutSelect((i) => i.input);
+ const deviceType = layoutSelect((i) => i.deviceType);
+ const isRTL = layoutSelect((i) => i.isRTL);
+ const fullscreen = layoutSelect((i) => i.fullscreen);
+ const fontSize = layoutSelect((i) => i.fontSize);
+ const currentPanelType = layoutSelect((i) => i.currentPanelType);
+
+ const presentationInput = layoutSelectInput((i) => i.presentation);
+ const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
+ const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
+ const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
+ const actionbarInput = layoutSelectInput((i) => i.actionBar);
+ const navbarInput = layoutSelectInput((i) => i.navBar);
+ const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
+ const screenShareInput = layoutSelectInput((i) => i.screenShare);
+ const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
+ const layoutContextDispatch = layoutDispatch();
+
+ const prevDeviceType = usePrevious(deviceType);
+
+ const throttledCalculatesLayout = throttle(() => calculatesLayout(), 50, {
+ trailing: true,
+ leading: true,
+ });
+
+ useEffect(() => {
+ window.addEventListener('resize', () => {
+ layoutContextDispatch({
+ type: ACTIONS.SET_BROWSER_SIZE,
+ value: {
+ width: window.document.documentElement.clientWidth,
+ height: window.document.documentElement.clientHeight,
+ },
+ });
+ });
+ }, []);
+
+ useEffect(() => {
+ if (deviceType === null) return () => null;
+
+ if (deviceType !== prevDeviceType) {
+ // reset layout if deviceType changed
+ // not all options is supported in all devices
+ init();
+ } else {
+ throttledCalculatesLayout();
+ }
+ }, [input, deviceType, isRTL, fontSize, fullscreen]);
+
+ const init = () => {
+ const { sidebarContentPanel } = sidebarContentInput;
+
+ if (isMobile) {
+ layoutContextDispatch({
+ type: ACTIONS.SET_LAYOUT_INPUT,
+ value: defaultsDeep(
+ {
+ sidebarNavigation: {
+ isOpen:
+ input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
+ },
+ sidebarContent: {
+ isOpen: sidebarContentPanel !== PANELS.NONE,
+ sidebarContentPanel,
+ },
+ SidebarContentHorizontalResizer: {
+ isOpen: false,
+ },
+ presentation: {
+ isOpen: presentationInput.isOpen,
+ slidesLength: presentationInput.slidesLength,
+ currentSlide: {
+ ...presentationInput.currentSlide,
+ },
+ },
+ cameraDock: {
+ numCameras: cameraDockInput.numCameras,
+ },
+ externalVideo: {
+ hasExternalVideo: externalVideoInput.hasExternalVideo,
+ },
+ screenShare: {
+ hasScreenShare: screenShareInput.hasScreenShare,
+ width: screenShareInput.width,
+ height: screenShareInput.height,
+ },
+ sharedNotes: {
+ isPinned: sharedNotesInput.isPinned,
+ },
+ },
+ INITIAL_INPUT_STATE
+ ),
+ });
+ } else {
+ layoutContextDispatch({
+ type: ACTIONS.SET_LAYOUT_INPUT,
+ value: defaultsDeep(
+ {
+ sidebarNavigation: {
+ isOpen:
+ input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
+ },
+ sidebarContent: {
+ isOpen: sidebarContentPanel !== PANELS.NONE,
+ sidebarContentPanel,
+ },
+ SidebarContentHorizontalResizer: {
+ isOpen: false,
+ },
+ presentation: {
+ isOpen: presentationInput.isOpen,
+ slidesLength: presentationInput.slidesLength,
+ currentSlide: {
+ ...presentationInput.currentSlide,
+ },
+ },
+ cameraDock: {
+ numCameras: cameraDockInput.numCameras,
+ },
+ externalVideo: {
+ hasExternalVideo: externalVideoInput.hasExternalVideo,
+ },
+ screenShare: {
+ hasScreenShare: screenShareInput.hasScreenShare,
+ width: screenShareInput.width,
+ height: screenShareInput.height,
+ },
+ sharedNotes: {
+ isPinned: sharedNotesInput.isPinned,
+ },
+ },
+ INITIAL_INPUT_STATE
+ ),
+ });
+ }
+ Session.set('layoutReady', true);
+ throttledCalculatesLayout();
+ };
+
+ const calculatesSidebarContentHeight = () => {
+ let sidebarContentHeight = 0;
+ if (sidebarContentInput.isOpen) {
+ if (isMobile) {
+ sidebarContentHeight = windowHeight() - DEFAULT_VALUES.navBarHeight;
+ } else {
+ sidebarContentHeight = windowHeight();
+ }
+ sidebarContentHeight -= bannerAreaHeight();
+ }
+ return sidebarContentHeight;
+ };
+
+ const calculatesCameraDockBounds = (mediaAreaBounds, mediaBounds, sidebarSize) => {
+ const { baseCameraDockBounds } = props;
+
+ const baseBounds = baseCameraDockBounds(mediaAreaBounds, sidebarSize);
+
+ // do not proceed if using values from LayoutEngine
+ if (Object.keys(baseBounds).length > 0) {
+ baseBounds.isCameraHorizontal = false;
+ return baseBounds;
+ }
+
+ const { camerasMargin, presentationToolbarMinWidth, navBarHeight } = DEFAULT_VALUES;
+
+ const cameraDockBounds = {};
+
+ cameraDockBounds.isCameraHorizontal = false;
+
+ const mediaBoundsWidth =
+ mediaBounds.width > presentationToolbarMinWidth && !isMobile
+ ? mediaBounds.width
+ : presentationToolbarMinWidth;
+
+ cameraDockBounds.top = navBarHeight;
+ cameraDockBounds.left = mediaAreaBounds.left;
+ cameraDockBounds.right = isRTL ? sidebarSize : null;
+ cameraDockBounds.zIndex = 1;
+
+ if (mediaBounds.width < mediaAreaBounds.width) {
+ cameraDockBounds.top = navBarHeight + bannerAreaHeight();
+ cameraDockBounds.width = mediaAreaBounds.width - mediaBoundsWidth;
+ cameraDockBounds.maxWidth = mediaAreaBounds.width * 0.8;
+ cameraDockBounds.height = mediaAreaBounds.height;
+ cameraDockBounds.maxHeight = mediaAreaBounds.height;
+ cameraDockBounds.left += camerasMargin;
+ cameraDockBounds.width -= camerasMargin * 2;
+ cameraDockBounds.isCameraHorizontal = true;
+ cameraDockBounds.position = CAMERADOCK_POSITION.CONTENT_LEFT;
+ // button size in vertical position
+ cameraDockBounds.height -= 20;
+ } else {
+ cameraDockBounds.width = mediaAreaBounds.width;
+ cameraDockBounds.maxWidth = mediaAreaBounds.width;
+ cameraDockBounds.height = mediaAreaBounds.height - mediaBounds.height;
+ cameraDockBounds.maxHeight = mediaAreaBounds.height * 0.8;
+ cameraDockBounds.top += camerasMargin;
+ cameraDockBounds.height -= camerasMargin * 2;
+ cameraDockBounds.position = CAMERADOCK_POSITION.CONTENT_TOP;
+ }
+
+ cameraDockBounds.minWidth = cameraDockBounds.width;
+ cameraDockBounds.minHeight = cameraDockBounds.height;
+
+ return cameraDockBounds;
+ };
+
+ const calculatesSlideSize = (mediaAreaBounds) => {
+ const { currentSlide } = presentationInput;
+
+ if (currentSlide.size.width === 0 && currentSlide.size.height === 0) {
+ return {
+ width: 0,
+ height: 0,
+ };
+ }
+
+ let slideWidth;
+ let slideHeight;
+
+ slideWidth = (currentSlide.size.width * mediaAreaBounds.height) / currentSlide.size.height;
+ slideHeight = mediaAreaBounds.height;
+
+ if (slideWidth > mediaAreaBounds.width) {
+ slideWidth = mediaAreaBounds.width;
+ slideHeight = (currentSlide.size.height * mediaAreaBounds.width) / currentSlide.size.width;
+ }
+
+ return {
+ width: slideWidth,
+ height: slideHeight,
+ };
+ };
+
+ const calculatesScreenShareSize = (mediaAreaBounds) => {
+ const { width = 0, height = 0 } = screenShareInput;
+
+ if (width === 0 && height === 0) return { width, height };
+
+ let screeShareWidth;
+ let screeShareHeight;
+
+ screeShareWidth = (width * mediaAreaBounds.height) / height;
+ screeShareHeight = mediaAreaBounds.height;
+
+ if (screeShareWidth > mediaAreaBounds.width) {
+ screeShareWidth = mediaAreaBounds.width;
+ screeShareHeight = (height * mediaAreaBounds.width) / width;
+ }
+
+ return {
+ width: screeShareWidth,
+ height: screeShareHeight,
+ };
+ };
+
+ const calculatesMediaBounds = (mediaAreaBounds, slideSize, sidebarSize, screenShareSize) => {
+ const { isOpen, slidesLength } = presentationInput;
+ const { hasExternalVideo } = externalVideoInput;
+ const { hasScreenShare } = screenShareInput;
+ const { isPinned: isSharedNotesPinned } = sharedNotesInput;
+
+ const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
+ const isGeneralMediaOff =
+ !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
+
+ const mediaBounds = {};
+ const { element: fullscreenElement } = fullscreen;
+
+ if (!isOpen || isGeneralMediaOff) {
+ mediaBounds.width = 0;
+ mediaBounds.height = 0;
+ mediaBounds.top = 0;
+ mediaBounds.left = !isRTL ? 0 : null;
+ mediaBounds.right = isRTL ? 0 : null;
+ mediaBounds.zIndex = 0;
+ return mediaBounds;
+ }
+
+ if (
+ fullscreenElement === 'Presentation' ||
+ fullscreenElement === 'Screenshare' ||
+ fullscreenElement === 'ExternalVideo'
+ ) {
+ mediaBounds.width = windowWidth();
+ mediaBounds.height = windowHeight();
+ mediaBounds.top = 0;
+ mediaBounds.left = !isRTL ? 0 : null;
+ mediaBounds.right = isRTL ? 0 : null;
+ mediaBounds.zIndex = 99;
+ return mediaBounds;
+ }
+
+ const mediaContentSize = hasScreenShare ? screenShareSize : slideSize;
+
+ if (cameraDockInput.numCameras > 0 && !cameraDockInput.isDragging) {
+ if (mediaContentSize.width !== 0 && mediaContentSize.height !== 0 && !hasExternalVideo) {
+ if (mediaContentSize.width < mediaAreaBounds.width && !isMobile) {
+ if (mediaContentSize.width < mediaAreaBounds.width * 0.8) {
+ mediaBounds.width = mediaContentSize.width;
+ } else {
+ mediaBounds.width = mediaAreaBounds.width * 0.8;
+ }
+ mediaBounds.height = mediaAreaBounds.height;
+ mediaBounds.top = mediaAreaBounds.top;
+ const sizeValue = mediaAreaBounds.left + (mediaAreaBounds.width - mediaBounds.width);
+ mediaBounds.left = !isRTL ? sizeValue : null;
+ mediaBounds.right = isRTL ? sidebarSize : null;
+ } else {
+ if (mediaContentSize.height < mediaAreaBounds.height * 0.8) {
+ mediaBounds.height = mediaContentSize.height;
+ } else {
+ mediaBounds.height = mediaAreaBounds.height * 0.8;
+ }
+ mediaBounds.width = mediaAreaBounds.width;
+ mediaBounds.top = mediaAreaBounds.top + (mediaAreaBounds.height - mediaBounds.height);
+ const sizeValue = mediaAreaBounds.left;
+ mediaBounds.left = !isRTL ? sizeValue : null;
+ mediaBounds.right = isRTL ? sidebarSize : null;
+ }
+ } else {
+ mediaBounds.width = mediaAreaBounds.width;
+ mediaBounds.height = mediaAreaBounds.height * 0.8;
+ mediaBounds.top = mediaAreaBounds.top + (mediaAreaBounds.height - mediaBounds.height);
+ const sizeValue = mediaAreaBounds.left;
+ mediaBounds.left = !isRTL ? sizeValue : null;
+ mediaBounds.right = isRTL ? sidebarSize : null;
+ }
+ } else {
+ mediaBounds.width = mediaAreaBounds.width;
+ mediaBounds.height = mediaAreaBounds.height;
+ mediaBounds.top = mediaAreaBounds.top;
+ const sizeValue = mediaAreaBounds.left;
+ mediaBounds.left = !isRTL ? sizeValue : null;
+ mediaBounds.right = isRTL ? sidebarSize : null;
+ }
+ mediaBounds.zIndex = 1;
+
+ return mediaBounds;
+ };
+
+ const calculatesLayout = () => {
+ const {
+ calculatesNavbarBounds,
+ calculatesActionbarBounds,
+ calculatesSidebarNavWidth,
+ calculatesSidebarNavHeight,
+ calculatesSidebarNavBounds,
+ calculatesSidebarContentWidth,
+ calculatesSidebarContentBounds,
+ calculatesMediaAreaBounds,
+ isTablet,
+ } = props;
+ const { camerasMargin, captionsMargin } = DEFAULT_VALUES;
+
+ const sidebarNavWidth = calculatesSidebarNavWidth();
+ const sidebarNavHeight = calculatesSidebarNavHeight();
+ const sidebarContentWidth = calculatesSidebarContentWidth();
+ const sidebarContentHeight = calculatesSidebarContentHeight();
+ const sidebarNavBounds = calculatesSidebarNavBounds();
+ const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
+ const mediaAreaBounds = calculatesMediaAreaBounds(
+ sidebarNavWidth.width,
+ sidebarContentWidth.width
+ );
+ const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
+ const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
+ const slideSize = calculatesSlideSize(mediaAreaBounds);
+ const screenShareSize = calculatesScreenShareSize(mediaAreaBounds);
+ const sidebarSize = sidebarContentWidth.width + sidebarNavWidth.width;
+ const mediaBounds = calculatesMediaBounds(
+ mediaAreaBounds,
+ slideSize,
+ sidebarSize,
+ screenShareSize
+ );
+ const cameraDockBounds = calculatesCameraDockBounds(mediaAreaBounds, mediaBounds, sidebarSize);
+ const horizontalCameraDiff = cameraDockBounds.isCameraHorizontal
+ ? cameraDockBounds.width + camerasMargin * 2
+ : 0;
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_NAVBAR_OUTPUT,
+ value: {
+ display: navbarInput.hasNavBar,
+ width: navbarBounds.width,
+ height: navbarBounds.height,
+ top: navbarBounds.top,
+ left: navbarBounds.left,
+ tabOrder: DEFAULT_VALUES.navBarTabOrder,
+ zIndex: navbarBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_ACTIONBAR_OUTPUT,
+ value: {
+ display: actionbarInput.hasActionBar,
+ width: actionbarBounds.width,
+ height: actionbarBounds.height,
+ innerHeight: actionbarBounds.innerHeight,
+ top: actionbarBounds.top,
+ left: actionbarBounds.left,
+ padding: actionbarBounds.padding,
+ tabOrder: DEFAULT_VALUES.actionBarTabOrder,
+ zIndex: actionbarBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAPTIONS_OUTPUT,
+ value: {
+ left: !isRTL ? sidebarSize + captionsMargin : null,
+ right: isRTL ? sidebarSize + captionsMargin : null,
+ maxWidth: mediaAreaBounds.width - captionsMargin * 2,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_NAVIGATION_OUTPUT,
+ value: {
+ display: sidebarNavigationInput.isOpen,
+ minWidth: sidebarNavWidth.minWidth,
+ width: sidebarNavWidth.width,
+ maxWidth: sidebarNavWidth.maxWidth,
+ height: sidebarNavHeight,
+ top: sidebarNavBounds.top,
+ left: sidebarNavBounds.left,
+ right: sidebarNavBounds.right,
+ tabOrder: DEFAULT_VALUES.sidebarNavTabOrder,
+ isResizable: !isMobile && !isTablet,
+ zIndex: sidebarNavBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_NAVIGATION_RESIZABLE_EDGE,
+ value: {
+ top: false,
+ right: !isRTL,
+ bottom: false,
+ left: isRTL,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_OUTPUT,
+ value: {
+ display: sidebarContentInput.isOpen,
+ minWidth: sidebarContentWidth.minWidth,
+ width: sidebarContentWidth.width,
+ maxWidth: sidebarContentWidth.maxWidth,
+ height: sidebarContentHeight,
+ top: sidebarContentBounds.top,
+ left: sidebarContentBounds.left,
+ right: sidebarContentBounds.right,
+ currentPanelType,
+ tabOrder: DEFAULT_VALUES.sidebarContentTabOrder,
+ isResizable: !isMobile && !isTablet,
+ zIndex: sidebarContentBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_RESIZABLE_EDGE,
+ value: {
+ top: false,
+ right: !isRTL,
+ bottom: false,
+ left: isRTL,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_MEDIA_AREA_SIZE,
+ value: {
+ width: mediaAreaBounds.width,
+ height: mediaAreaBounds.height,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAMERA_DOCK_OUTPUT,
+ value: {
+ display: cameraDockInput.numCameras > 0,
+ position: cameraDockBounds.position,
+ minWidth: cameraDockBounds.minWidth,
+ width: cameraDockBounds.width,
+ maxWidth: cameraDockBounds.maxWidth,
+ minHeight: cameraDockBounds.minHeight,
+ height: cameraDockBounds.height,
+ maxHeight: cameraDockBounds.maxHeight,
+ top: cameraDockBounds.top,
+ left: cameraDockBounds.left,
+ right: cameraDockBounds.right,
+ tabOrder: 4,
+ isDraggable: false,
+ resizableEdge: {
+ top: false,
+ right: false,
+ bottom: false,
+ left: false,
+ },
+ zIndex: cameraDockBounds.zIndex,
+ focusedId: input.cameraDock.focusedId,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_PRESENTATION_OUTPUT,
+ value: {
+ display: presentationInput.isOpen,
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
+ tabOrder: DEFAULT_VALUES.presentationTabOrder,
+ isResizable: false,
+ zIndex: mediaBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SCREEN_SHARE_OUTPUT,
+ value: {
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
+ zIndex: mediaBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_EXTERNAL_VIDEO_OUTPUT,
+ value: {
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
+ value: {
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right + horizontalCameraDiff : null,
+ },
+ });
+ };
+
+ return null;
+};
+
+export default SmartLayout;
\ No newline at end of file
diff --git a/src/2.7.12/imports/ui/components/layout/layout-manager/videoFocusLayout.jsx b/src/2.7.12/imports/ui/components/layout/layout-manager/videoFocusLayout.jsx
new file mode 100644
index 00000000..16bc993e
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/layout-manager/videoFocusLayout.jsx
@@ -0,0 +1,524 @@
+import { useEffect, useRef } from 'react';
+import { throttle } from '/imports/utils/throttle';
+import {
+ layoutDispatch,
+ layoutSelect,
+ layoutSelectInput,
+ layoutSelectOutput,
+} from '/imports/ui/components/layout/context';
+import DEFAULT_VALUES from '/imports/ui/components/layout/defaultValues';
+import { INITIAL_INPUT_STATE } from '/imports/ui/components/layout/initState';
+import { ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
+import { defaultsDeep } from '/imports/utils/array-utils';
+import { isPresentationEnabled } from '/imports/ui/services/features';
+
+const windowWidth = () => window.document.documentElement.clientWidth;
+const windowHeight = () => window.document.documentElement.clientHeight;
+
+const VideoFocusLayout = (props) => {
+ const { bannerAreaHeight, isMobile } = props;
+
+ function usePrevious(value) {
+ const ref = useRef();
+ useEffect(() => {
+ ref.current = value;
+ });
+ return ref.current;
+ }
+
+ const input = layoutSelect((i) => i.input);
+ const deviceType = layoutSelect((i) => i.deviceType);
+ const isRTL = layoutSelect((i) => i.isRTL);
+ const fullscreen = layoutSelect((i) => i.fullscreen);
+ const fontSize = layoutSelect((i) => i.fontSize);
+ const currentPanelType = layoutSelect((i) => i.currentPanelType);
+
+ const presentationInput = layoutSelectInput((i) => i.presentation);
+ const externalVideoInput = layoutSelectInput((i) => i.externalVideo);
+ const screenShareInput = layoutSelectInput((i) => i.screenShare);
+ const sharedNotesInput = layoutSelectInput((i) => i.sharedNotes);
+
+ const sidebarNavigationInput = layoutSelectInput((i) => i.sidebarNavigation);
+ const sidebarContentInput = layoutSelectInput((i) => i.sidebarContent);
+ const cameraDockInput = layoutSelectInput((i) => i.cameraDock);
+ const actionbarInput = layoutSelectInput((i) => i.actionBar);
+ const navbarInput = layoutSelectInput((i) => i.navBar);
+ const layoutContextDispatch = layoutDispatch();
+
+ const sidebarContentOutput = layoutSelectOutput((i) => i.sidebarContent);
+
+ const prevDeviceType = usePrevious(deviceType);
+
+ const throttledCalculatesLayout = throttle(() => calculatesLayout(), 50, {
+ trailing: true,
+ leading: true,
+ });
+
+ useEffect(() => {
+ window.addEventListener('resize', () => {
+ layoutContextDispatch({
+ type: ACTIONS.SET_BROWSER_SIZE,
+ value: {
+ width: window.document.documentElement.clientWidth,
+ height: window.document.documentElement.clientHeight,
+ },
+ });
+ });
+ }, []);
+
+ useEffect(() => {
+ if (deviceType === null) return () => null;
+
+ if (deviceType !== prevDeviceType) {
+ // reset layout if deviceType changed
+ // not all options is supported in all devices
+ init();
+ } else {
+ throttledCalculatesLayout();
+ }
+ }, [input, deviceType, isRTL, fontSize, fullscreen]);
+
+ const init = () => {
+ const { sidebarContentPanel } = sidebarContentInput;
+ if (isMobile) {
+ layoutContextDispatch({
+ type: ACTIONS.SET_LAYOUT_INPUT,
+ value: defaultsDeep(
+ {
+ sidebarNavigation: {
+ isOpen:
+ input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
+ },
+ sidebarContent: {
+ isOpen: sidebarContentPanel !== PANELS.NONE,
+ sidebarContentPanel,
+ },
+ SidebarContentHorizontalResizer: {
+ isOpen: false,
+ },
+ presentation: {
+ isOpen: presentationInput.isOpen,
+ slidesLength: presentationInput.slidesLength,
+ currentSlide: {
+ ...presentationInput.currentSlide,
+ },
+ },
+ cameraDock: {
+ numCameras: cameraDockInput.numCameras,
+ },
+ externalVideo: {
+ hasExternalVideo: input.externalVideo.hasExternalVideo,
+ },
+ screenShare: {
+ hasScreenShare: input.screenShare.hasScreenShare,
+ width: input.screenShare.width,
+ height: input.screenShare.height,
+ },
+ },
+ INITIAL_INPUT_STATE
+ ),
+ });
+ } else {
+ layoutContextDispatch({
+ type: ACTIONS.SET_LAYOUT_INPUT,
+ value: defaultsDeep(
+ {
+ sidebarNavigation: {
+ isOpen:
+ input.sidebarNavigation.isOpen || sidebarContentPanel !== PANELS.NONE || false,
+ },
+ sidebarContent: {
+ isOpen: sidebarContentPanel !== PANELS.NONE,
+ sidebarContentPanel,
+ },
+ SidebarContentHorizontalResizer: {
+ isOpen: false,
+ },
+ presentation: {
+ isOpen: presentationInput.isOpen,
+ slidesLength: presentationInput.slidesLength,
+ currentSlide: {
+ ...presentationInput.currentSlide,
+ },
+ },
+ cameraDock: {
+ numCameras: cameraDockInput.numCameras,
+ },
+ externalVideo: {
+ hasExternalVideo: input.externalVideo.hasExternalVideo,
+ },
+ screenShare: {
+ hasScreenShare: input.screenShare.hasScreenShare,
+ width: input.screenShare.width,
+ height: input.screenShare.height,
+ },
+ },
+ INITIAL_INPUT_STATE
+ ),
+ });
+ }
+ Session.set('layoutReady', true);
+ throttledCalculatesLayout();
+ };
+
+ const calculatesSidebarContentHeight = () => {
+ const { isOpen, slidesLength } = presentationInput;
+ const { hasExternalVideo } = externalVideoInput;
+ const { hasScreenShare } = screenShareInput;
+ const { isPinned: isSharedNotesPinned } = sharedNotesInput;
+
+ const hasPresentation = isPresentationEnabled() && slidesLength !== 0;
+ const isGeneralMediaOff =
+ !hasPresentation && !hasExternalVideo && !hasScreenShare && !isSharedNotesPinned;
+
+ let minHeight = 0;
+ let height = 0;
+ let maxHeight = 0;
+ if (sidebarContentInput.isOpen) {
+ if (isMobile) {
+ height = windowHeight() - DEFAULT_VALUES.navBarHeight - bannerAreaHeight();
+ minHeight = height;
+ maxHeight = height;
+ } else if (isOpen && !isGeneralMediaOff) {
+ if (sidebarContentInput.height > 0 && sidebarContentInput.height < windowHeight()) {
+ height = sidebarContentInput.height - bannerAreaHeight();
+ } else {
+ const { size: slideSize } = presentationInput.currentSlide;
+ let calculatedHeight = (windowHeight() - bannerAreaHeight()) * 0.3;
+
+ if (slideSize.height > 0 && slideSize.width > 0) {
+ calculatedHeight = (slideSize.height * sidebarContentOutput.width) / slideSize.width;
+ }
+ height = windowHeight() - calculatedHeight - bannerAreaHeight();
+ }
+ maxHeight = windowHeight() * 0.75 - bannerAreaHeight();
+ minHeight = windowHeight() * 0.25 - bannerAreaHeight();
+
+ if (height > maxHeight) {
+ height = maxHeight;
+ }
+ } else {
+ height = windowHeight() - bannerAreaHeight();
+ maxHeight = height;
+ minHeight = height;
+ }
+ }
+ return {
+ minHeight,
+ height,
+ maxHeight,
+ };
+ };
+
+ const calculatesCameraDockBounds = (mediaAreaBounds, sidebarSize) => {
+ const { baseCameraDockBounds } = props;
+
+ const baseBounds = baseCameraDockBounds(mediaAreaBounds, sidebarSize);
+
+ // do not proceed if using values from LayoutEngine
+ if (Object.keys(baseBounds).length > 0) {
+ return baseBounds;
+ }
+
+ const { navBarHeight } = DEFAULT_VALUES;
+
+ const cameraDockBounds = {};
+
+ const mobileCameraHeight = mediaAreaBounds.height * 0.7 - bannerAreaHeight();
+ const cameraHeight = mediaAreaBounds.height - bannerAreaHeight();
+
+ if (isMobile) {
+ cameraDockBounds.minHeight = mobileCameraHeight;
+ cameraDockBounds.height = mobileCameraHeight;
+ cameraDockBounds.maxHeight = mobileCameraHeight;
+ } else {
+ cameraDockBounds.minHeight = cameraHeight;
+ cameraDockBounds.height = cameraHeight;
+ cameraDockBounds.maxHeight = cameraHeight;
+ }
+
+ cameraDockBounds.top = navBarHeight + bannerAreaHeight();
+ cameraDockBounds.left = !isRTL ? mediaAreaBounds.left : null;
+ cameraDockBounds.right = isRTL ? sidebarSize : null;
+ cameraDockBounds.minWidth = mediaAreaBounds.width;
+ cameraDockBounds.width = mediaAreaBounds.width;
+ cameraDockBounds.maxWidth = mediaAreaBounds.width;
+ cameraDockBounds.zIndex = 1;
+
+ return cameraDockBounds;
+ };
+
+ const calculatesMediaBounds = (
+ mediaAreaBounds,
+ cameraDockBounds,
+ sidebarNavWidth,
+ sidebarContentWidth,
+ sidebarContentHeight
+ ) => {
+ const mediaBounds = {};
+ const { element: fullscreenElement } = fullscreen;
+
+ if (
+ fullscreenElement === 'Presentation' ||
+ fullscreenElement === 'Screenshare' ||
+ fullscreenElement === 'ExternalVideo'
+ ) {
+ mediaBounds.width = windowWidth();
+ mediaBounds.height = windowHeight();
+ mediaBounds.top = 0;
+ mediaBounds.left = 0;
+ mediaBounds.right = 0;
+ mediaBounds.zIndex = 99;
+ return mediaBounds;
+ }
+
+ if (isMobile) {
+ mediaBounds.height = mediaAreaBounds.height - cameraDockBounds.height;
+ mediaBounds.left = mediaAreaBounds.left;
+ mediaBounds.top = mediaAreaBounds.top + cameraDockBounds.height;
+ mediaBounds.width = mediaAreaBounds.width;
+ } else if (presentationInput.isOpen) {
+ mediaBounds.height = windowHeight() - sidebarContentHeight - bannerAreaHeight();
+ mediaBounds.left = !isRTL ? sidebarNavWidth : 0;
+ mediaBounds.right = isRTL ? sidebarNavWidth : 0;
+ mediaBounds.top = sidebarContentHeight + bannerAreaHeight();
+ mediaBounds.width = sidebarContentWidth;
+ mediaBounds.zIndex = 1;
+ } else if (!presentationInput.isOpen) {
+ mediaBounds.width = 0;
+ mediaBounds.height = 0;
+ mediaBounds.top = 0;
+ mediaBounds.left = 0;
+ }
+
+ return mediaBounds;
+ };
+
+ const calculatesLayout = () => {
+ const {
+ calculatesNavbarBounds,
+ calculatesActionbarBounds,
+ calculatesSidebarNavWidth,
+ calculatesSidebarNavHeight,
+ calculatesSidebarNavBounds,
+ calculatesSidebarContentWidth,
+ calculatesSidebarContentBounds,
+ calculatesMediaAreaBounds,
+ isTablet,
+ } = props;
+ const { captionsMargin } = DEFAULT_VALUES;
+
+ const sidebarNavWidth = calculatesSidebarNavWidth();
+ const sidebarNavHeight = calculatesSidebarNavHeight();
+ const sidebarContentWidth = calculatesSidebarContentWidth();
+ const sidebarNavBounds = calculatesSidebarNavBounds();
+ const sidebarContentBounds = calculatesSidebarContentBounds(sidebarNavWidth.width);
+ const mediaAreaBounds = calculatesMediaAreaBounds(
+ sidebarNavWidth.width,
+ sidebarContentWidth.width
+ );
+ const navbarBounds = calculatesNavbarBounds(mediaAreaBounds);
+ const actionbarBounds = calculatesActionbarBounds(mediaAreaBounds);
+ const sidebarSize = sidebarContentWidth.width + sidebarNavWidth.width;
+ const cameraDockBounds = calculatesCameraDockBounds(mediaAreaBounds, sidebarSize);
+ const sidebarContentHeight = calculatesSidebarContentHeight();
+ const mediaBounds = calculatesMediaBounds(
+ mediaAreaBounds,
+ cameraDockBounds,
+ sidebarNavWidth.width,
+ sidebarContentWidth.width,
+ sidebarContentHeight.height
+ );
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_NAVBAR_OUTPUT,
+ value: {
+ display: navbarInput.hasNavBar,
+ width: navbarBounds.width,
+ height: navbarBounds.height,
+ top: navbarBounds.top,
+ left: navbarBounds.left,
+ tabOrder: DEFAULT_VALUES.navBarTabOrder,
+ zIndex: navbarBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_ACTIONBAR_OUTPUT,
+ value: {
+ display: actionbarInput.hasActionBar,
+ width: actionbarBounds.width,
+ height: actionbarBounds.height,
+ innerHeight: actionbarBounds.innerHeight,
+ top: actionbarBounds.top,
+ left: actionbarBounds.left,
+ padding: actionbarBounds.padding,
+ tabOrder: DEFAULT_VALUES.actionBarTabOrder,
+ zIndex: actionbarBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAPTIONS_OUTPUT,
+ value: {
+ left: !isRTL ? sidebarSize + captionsMargin : null,
+ right: isRTL ? sidebarSize + captionsMargin : null,
+ maxWidth: mediaAreaBounds.width - captionsMargin * 2,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_NAVIGATION_OUTPUT,
+ value: {
+ display: sidebarNavigationInput.isOpen,
+ minWidth: sidebarNavWidth.minWidth,
+ width: sidebarNavWidth.width,
+ maxWidth: sidebarNavWidth.maxWidth,
+ height: sidebarNavHeight,
+ top: sidebarNavBounds.top,
+ left: sidebarNavBounds.left,
+ right: sidebarNavBounds.right,
+ tabOrder: DEFAULT_VALUES.sidebarNavTabOrder,
+ isResizable: !isMobile && !isTablet,
+ zIndex: sidebarNavBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_NAVIGATION_RESIZABLE_EDGE,
+ value: {
+ top: false,
+ right: !isRTL,
+ bottom: false,
+ left: isRTL,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_OUTPUT,
+ value: {
+ display: sidebarContentInput.isOpen,
+ minWidth: sidebarContentWidth.minWidth,
+ width: sidebarContentWidth.width,
+ maxWidth: sidebarContentWidth.maxWidth,
+ minHeight: sidebarContentHeight.minHeight,
+ height: sidebarContentHeight.height,
+ maxHeight: sidebarContentHeight.maxHeight,
+ top: sidebarContentBounds.top,
+ left: sidebarContentBounds.left,
+ right: sidebarContentBounds.right,
+ currentPanelType,
+ tabOrder: DEFAULT_VALUES.sidebarContentTabOrder,
+ isResizable: !isMobile && !isTablet,
+ zIndex: sidebarContentBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_RESIZABLE_EDGE,
+ value: {
+ top: false,
+ right: !isRTL,
+ bottom: true,
+ left: isRTL,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_MEDIA_AREA_SIZE,
+ value: {
+ width: mediaAreaBounds.width,
+ height: mediaAreaBounds.height,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAMERA_DOCK_OUTPUT,
+ value: {
+ display: true,
+ minWidth: cameraDockBounds.minWidth,
+ width: cameraDockBounds.width,
+ maxWidth: cameraDockBounds.maxWidth,
+ minHeight: cameraDockBounds.minHeight,
+ height: cameraDockBounds.height,
+ maxHeight: cameraDockBounds.maxHeight,
+ top: cameraDockBounds.top,
+ left: cameraDockBounds.left,
+ right: cameraDockBounds.right,
+ tabOrder: 4,
+ isDraggable: false,
+ resizableEdge: {
+ top: false,
+ right: false,
+ bottom: false,
+ left: false,
+ },
+ zIndex: cameraDockBounds.zIndex,
+ focusedId: input.cameraDock.focusedId,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_PRESENTATION_OUTPUT,
+ value: {
+ display: presentationInput.isOpen,
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right : null,
+ tabOrder: DEFAULT_VALUES.presentationTabOrder,
+ isResizable: !isMobile && !isTablet,
+ zIndex: mediaBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_PRESENTATION_RESIZABLE_EDGE,
+ value: {
+ top: true,
+ right: false,
+ bottom: false,
+ left: false,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SCREEN_SHARE_OUTPUT,
+ value: {
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right : null,
+ zIndex: mediaBounds.zIndex,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_EXTERNAL_VIDEO_OUTPUT,
+ value: {
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right : null,
+ },
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_SHARED_NOTES_OUTPUT,
+ value: {
+ width: mediaBounds.width,
+ height: mediaBounds.height,
+ top: mediaBounds.top,
+ left: mediaBounds.left,
+ right: isRTL ? mediaBounds.right : null,
+ },
+ });
+ };
+
+ return null;
+};
+
+export default VideoFocusLayout;
\ No newline at end of file
diff --git a/src/2.7.12/imports/ui/components/layout/modal/component.jsx b/src/2.7.12/imports/ui/components/layout/modal/component.jsx
new file mode 100644
index 00000000..7494f57d
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/modal/component.jsx
@@ -0,0 +1,201 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+import { LAYOUT_TYPE, CAMERADOCK_POSITION } from '/imports/ui/components/layout/enums';
+import SettingsService from '/imports/ui/components/settings/service';
+import deviceInfo from '/imports/utils/deviceInfo';
+import Button from '/imports/ui/components/common/button/component';
+import Styled from './styles';
+
+const LayoutModalComponent = (props) => {
+ const {
+ intl,
+ setIsOpen,
+ isModerator,
+ isPresenter,
+ application,
+ updateSettings,
+ onRequestClose,
+ isOpen,
+ } = props;
+
+ const [selectedLayout, setSelectedLayout] = useState(application.selectedLayout);
+ const [updateAllUsed, setUpdateAllUsed] = useState(false);
+
+ const BASE_NAME = Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename;
+
+ const LAYOUTS_PATH = `${BASE_NAME}/resources/images/layouts/`;
+ const isKeepPushingLayoutEnabled = SettingsService.isKeepPushingLayoutEnabled();
+
+ const intlMessages = defineMessages({
+ title: {
+ id: 'app.layout.modal.title',
+ description: 'Modal title',
+ },
+ update: {
+ id: 'app.layout.modal.update',
+ description: 'Modal confirm button',
+ },
+ updateAll: {
+ id: 'app.layout.modal.updateAll',
+ description: 'Modal updateAll button',
+ },
+ layoutLabel: {
+ id: 'app.layout.modal.layoutLabel',
+ description: 'Layout label',
+ },
+ layoutToastLabelAuto: {
+ id: 'app.layout.modal.layoutToastLabelAuto',
+ description: 'Layout toast label',
+ },
+ layoutToastLabel: {
+ id: 'app.layout.modal.layoutToastLabel',
+ description: 'Layout toast label',
+ },
+ layoutToastLabelAutoOff: {
+ id: 'app.layout.modal.layoutToastLabelAutoOff',
+ description: 'Layout toast label',
+ },
+ customLayout: {
+ id: 'app.layout.style.custom',
+ description: 'label for custom layout style',
+ },
+ smartLayout: {
+ id: 'app.layout.style.smart',
+ description: 'label for smart layout style',
+ },
+ presentationFocusLayout: {
+ id: 'app.layout.style.presentationFocus',
+ description: 'label for presentationFocus layout style',
+ },
+ videoFocusLayout: {
+ id: 'app.layout.style.videoFocus',
+ description: 'label for videoFocus layout style',
+ },
+ layoutSingular: {
+ id: 'app.layout.modal.layoutSingular',
+ description: 'label for singular layout',
+ },
+ layoutBtnDesc: {
+ id: 'app.layout.modal.layoutBtnDesc',
+ description: 'label for singular layout',
+ },
+ });
+
+ const handleSwitchLayout = (e) => {
+ setSelectedLayout(e);
+ };
+
+ const handleUpdateLayout = (updateAll) => {
+ const obj = {
+ application:
+ { ...application, selectedLayout, pushLayout: updateAll },
+ };
+ if ((isModerator || isPresenter) && updateAll) {
+ updateSettings(obj, intlMessages.layoutToastLabelAuto);
+ setUpdateAllUsed(true);
+ } else if ((isModerator || isPresenter) && !updateAll && !updateAllUsed) {
+ updateSettings(obj, intlMessages.layoutToastLabelAutoOff);
+ setUpdateAllUsed(false);
+ } else {
+ updateSettings(obj, intlMessages.layoutToastLabel);
+ }
+ setIsOpen(false);
+ };
+
+ const renderPushLayoutsOptions = () => {
+ if (!isModerator && !isPresenter) {
+ return null;
+ }
+
+ if (isKeepPushingLayoutEnabled) {
+ return (
+ handleUpdateLayout(true)}
+ color="secondary"
+ data-test="updateEveryoneLayoutBtn"
+ />
+ );
+ }
+ return null;
+ };
+
+ const renderLayoutButtons = () => (
+
+ {Object.values(LAYOUT_TYPE)
+ .map((layout) => (
+
+
+ )}
+ onClick={() => {
+ handleSwitchLayout(layout);
+ if (layout === LAYOUT_TYPE.CUSTOM_LAYOUT && application.selectedLayout !== layout) {
+ document.getElementById('layout')?.setAttribute('data-cam-position', CAMERADOCK_POSITION.CONTENT_TOP);
+ }
+ }}
+ active={(layout === selectedLayout).toString()}
+ aria-describedby="layout-btn-desc"
+ data-test={`${layout}Layout`}
+ />
+ {intl.formatMessage(intlMessages[`${layout}Layout`])}
+
+ ))}
+
+ );
+
+ return (
+ setIsOpen(false)}
+ title={intl.formatMessage(intlMessages.title)}
+ {...{
+ isOpen,
+ onRequestClose,
+ }}
+ >
+
+
+ {renderLayoutButtons()}
+
+
+
+ {renderPushLayoutsOptions()}
+ handleUpdateLayout(false)}
+ data-test="updateLayoutBtn"
+ />
+
+ {intl.formatMessage(intlMessages.layoutBtnDesc)}
+
+ );
+};
+
+const propTypes = {
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func.isRequired,
+ }).isRequired,
+ isModerator: PropTypes.bool.isRequired,
+ isPresenter: PropTypes.bool.isRequired,
+ showToggleLabel: PropTypes.bool.isRequired,
+ application: PropTypes.shape({
+ selectedLayout: PropTypes.string.isRequired,
+ }).isRequired,
+ updateSettings: PropTypes.func.isRequired,
+};
+
+LayoutModalComponent.propTypes = propTypes;
+
+export default injectIntl(LayoutModalComponent);
diff --git a/src/2.7.12/imports/ui/components/layout/modal/container.jsx b/src/2.7.12/imports/ui/components/layout/modal/container.jsx
new file mode 100644
index 00000000..f38aa8ce
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/modal/container.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import SettingsService from '/imports/ui/services/settings';
+import LayoutModalComponent from './component';
+
+import {
+ updateSettings,
+ isPresenter,
+} from '/imports/ui/components/settings/service';
+
+const LayoutModalContainer = (props) => {
+ const { intl, setIsOpen,onRequestClose, isOpen, isModerator, isPresenter,
+ application, updateSettings, } = props;
+ return };
+
+export default withTracker(({ amIModerator }) => ({
+ application: SettingsService.application,
+ updateSettings,
+ isPresenter: isPresenter(),
+ isModerator: amIModerator,
+}))(LayoutModalContainer);
diff --git a/src/2.7.12/imports/ui/components/layout/modal/styles.js b/src/2.7.12/imports/ui/components/layout/modal/styles.js
new file mode 100644
index 00000000..a8e29a15
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/modal/styles.js
@@ -0,0 +1,190 @@
+import styled from 'styled-components';
+import {
+ colorPrimary,
+ colorWhite,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
+import Button from '/imports/ui/components/common/button/component';
+import ModalSimple from '/imports/ui/components/common/modal/simple/component';
+import ModalStyles from '/imports/ui/components/common/modal/simple/styles';
+
+const Content = styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ padding: .5rem 0 .5rem 0;
+ overflow: hidden;
+`;
+
+const LayoutModal = styled(ModalSimple)`
+ padding: 1rem;
+
+ @media ${smallOnly} {
+ height: unset;
+ }
+
+ ${({ isPhone }) => isPhone && `
+ min-height: 100%;
+ min-width: 100%;
+ border-radius: 0;
+ `}
+
+ ${ModalStyles.Content} {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ }
+`;
+
+const BodyContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+const IconSvg = styled.img`
+ height: 8rem;
+ border-radius: 5px;
+ margin: 5px;
+
+ @media ${smallOnly} {
+ height: 3rem;
+ margin: 1px;
+ }
+`;
+
+const LayoutBtn = styled(Button)`
+ display: flex;
+ box-shadow: unset !important;
+ background-color: ${colorWhite};
+ border: ${colorWhite} solid 6px;
+ align-items: center;
+ flex-direction: column;
+ padding: 0 !important;
+ margin: 1rem 1rem 0.5rem 1rem;
+ width: fit-content;
+
+ @media ${smallOnly} {
+ margin: 0.5rem;
+ border: ${colorWhite} solid 4px;
+ border-radius: 10px;
+ width: fit-content;
+ }
+
+ &:focus,
+ &:hover {
+ border: ${colorPrimary} solid 6px;
+ border-radius: 5px;
+ }
+
+ ${({ active }) => (active === 'true') && `
+ border: ${colorPrimary} solid 6px;
+ border-radius: 5px;
+
+ @media ${smallOnly} {
+ border: ${colorPrimary} solid 4px;
+ border-radius: 5px;
+ }
+
+ &:before {
+ font-family: 'bbb-icons';
+ color: ${colorWhite};
+ position: fixed;
+ content: "\\e946";
+ background-color: ${colorPrimary};
+ margin-left: 13.1rem;
+ padding: 0.3rem 0.2rem 0 0.6rem;
+ border-radius: 0 0 0 .3rem;
+
+ [dir="rtl"] & {
+ left: auto;
+ margin-right: 13.1rem;
+ margin-left: unset;
+ padding: 0.3rem 0.6rem 0 0.2rem;
+ border-radius: 0 0 .3rem 0;
+ }
+ width: 1.8rem;
+ height: 1.8rem;
+
+ @media ${smallOnly} {
+ width: 1rem;
+ height: 1rem;
+ font-size: 0.6rem;
+ margin-left: 4.5rem;
+ padding: 0.2rem 0.2rem 0 0.3rem;
+
+ [dir="rtl"] & {
+ margin-right: 4.5rem;
+ margin-left: unset;
+ padding: 0.2rem 0.3rem 0 0.2rem;
+ }
+ }
+ }
+ `};
+`;
+
+const ButtonLayoutContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ @media ${smallOnly} {
+ align-items: center;
+ }
+`;
+
+const ButtonsContainer = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ @media ${smallOnly} {
+ flex-wrap: unset;
+ flex-direction: column;
+ align-items: center;
+ }
+`;
+
+const ButtonBottomContainer = styled.div`
+ align-self: end;
+ padding-right: 3rem;
+ padding-top: 1rem;
+
+ @media ${smallOnly} {
+ align-self: center;
+ padding-right: unset;
+ }
+`;
+
+const LabelLayoutNames = styled.label`
+ text-align: center;
+ margin: 0 0 0.1rem 0;
+`;
+
+const BottomButton = styled(Button)`
+ margin: 0 0.5rem;
+`;
+
+const PushContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 1rem 0 1rem 0;
+`;
+
+const LabelPushLayout = styled.div`
+ padding-right: 0.5rem;
+`;
+
+export default {
+ Content,
+ LayoutModal,
+ BodyContainer,
+ IconSvg,
+ LayoutBtn,
+ ButtonLayoutContainer,
+ ButtonsContainer,
+ ButtonBottomContainer,
+ LabelLayoutNames,
+ BottomButton,
+ PushContainer,
+ LabelPushLayout,
+};
diff --git a/src/2.7.12/imports/ui/components/layout/push-layout/pushLayoutEngine.jsx b/src/2.7.12/imports/ui/components/layout/push-layout/pushLayoutEngine.jsx
new file mode 100644
index 00000000..2d815b5e
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/push-layout/pushLayoutEngine.jsx
@@ -0,0 +1,274 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Meteor } from 'meteor/meteor';
+import getFromUserSettings from '/imports/ui/services/users-settings';
+import Settings from '/imports/ui/services/settings';
+import MediaService from '/imports/ui/components/media/service';
+import { LAYOUT_TYPE, ACTIONS } from '../enums';
+import { isMobile } from '../utils';
+import { updateSettings } from '/imports/ui/components/settings/service';
+import { Session } from 'meteor/session';
+
+const HIDE_PRESENTATION = Meteor.settings.public.layout.hidePresentationOnJoin;
+
+const equalDouble = (n1, n2) => {
+ const precision = 0.01;
+
+ return Math.abs(n1 - n2) <= precision;
+};
+
+const propTypes = {
+ cameraWidth: PropTypes.number,
+ cameraHeight: PropTypes.number,
+ cameraIsResizing: PropTypes.bool,
+ cameraPosition: PropTypes.string,
+ focusedCamera: PropTypes.string,
+ horizontalPosition: PropTypes.bool,
+ isMeetingLayoutResizing: PropTypes.bool,
+ isPresenter: PropTypes.bool,
+ isModerator: PropTypes.bool,
+ layoutContextDispatch: PropTypes.func,
+ meetingLayout: PropTypes.string,
+ meetingLayoutCameraPosition: PropTypes.string,
+ meetingLayoutFocusedCamera: PropTypes.string,
+ meetingLayoutVideoRate: PropTypes.number,
+ meetingPresentationIsOpen: PropTypes.bool,
+ meetingLayoutUpdatedAt: PropTypes.number,
+ presentationIsOpen: PropTypes.bool,
+ presentationVideoRate: PropTypes.number,
+ pushLayout: PropTypes.bool,
+ pushLayoutMeeting: PropTypes.bool,
+ selectedLayout: PropTypes.string,
+ setMeetingLayout: PropTypes.func,
+ setPushLayout: PropTypes.func,
+ shouldShowScreenshare: PropTypes.bool,
+ shouldShowExternalVideo: PropTypes.bool,
+};
+
+class PushLayoutEngine extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ componentDidMount() {
+ const {
+ cameraWidth,
+ cameraHeight,
+ horizontalPosition,
+ layoutContextDispatch,
+ meetingLayoutCameraPosition,
+ meetingLayoutFocusedCamera,
+ meetingLayoutVideoRate,
+ meetingPresentationIsOpen,
+ shouldShowScreenshare,
+ shouldShowExternalVideo,
+ } = this.props;
+
+ const changedLayout = getFromUserSettings('bbb_change_layout', null);
+ if (changedLayout) {
+ Settings.application.selectedLayout = LAYOUT_TYPE[changedLayout];
+ }
+
+ let { selectedLayout } = Settings.application;
+ if (isMobile()) {
+ selectedLayout = selectedLayout === 'custom' ? 'smart' : selectedLayout;
+ Settings.application.selectedLayout = selectedLayout;
+ }
+ Session.set('isGridEnabled', selectedLayout === LAYOUT_TYPE.VIDEO_FOCUS);
+
+ Settings.save();
+
+ const initialPresentation = !getFromUserSettings('bbb_hide_presentation_on_join', HIDE_PRESENTATION || !meetingPresentationIsOpen) || shouldShowScreenshare || shouldShowExternalVideo;
+ MediaService.setPresentationIsOpen(layoutContextDispatch, initialPresentation);
+ Session.set('presentationLastState', initialPresentation);
+
+ if (selectedLayout === 'custom') {
+ setTimeout(() => {
+ layoutContextDispatch({
+ type: ACTIONS.SET_FOCUSED_CAMERA_ID,
+ value: meetingLayoutFocusedCamera,
+ });
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAMERA_DOCK_POSITION,
+ value: meetingLayoutCameraPosition,
+ });
+
+ if (!equalDouble(meetingLayoutVideoRate, 0)) {
+ let w, h;
+ if (horizontalPosition) {
+ w = window.innerWidth * meetingLayoutVideoRate;
+ h = cameraHeight;
+ } else {
+ w = cameraWidth;
+ h = window.innerHeight * meetingLayoutVideoRate;
+ }
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAMERA_DOCK_SIZE,
+ value: {
+ width: w,
+ height: h,
+ browserWidth: window.innerWidth,
+ browserHeight: window.innerHeight,
+ }
+ });
+ }
+ }, 0);
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ const {
+ cameraWidth,
+ cameraHeight,
+ cameraIsResizing,
+ cameraPosition,
+ focusedCamera,
+ horizontalPosition,
+ isMeetingLayoutResizing,
+ isModerator,
+ isPresenter,
+ layoutContextDispatch,
+ meetingLayout,
+ meetingLayoutUpdatedAt,
+ meetingPresentationIsOpen,
+ meetingLayoutCameraPosition,
+ meetingLayoutFocusedCamera,
+ meetingLayoutVideoRate,
+ presentationIsOpen,
+ presentationVideoRate,
+ pushLayout,
+ pushLayoutMeeting,
+ selectedLayout,
+ setMeetingLayout,
+ setPushLayout,
+ } = this.props;
+
+ const meetingLayoutDidChange = meetingLayout !== prevProps.meetingLayout;
+ const pushLayoutMeetingDidChange = pushLayoutMeeting !== prevProps.pushLayoutMeeting;
+ const shouldSwitchLayout = isPresenter
+ ? meetingLayoutDidChange
+ : (meetingLayoutDidChange || pushLayoutMeetingDidChange) && pushLayoutMeeting;
+
+ if (shouldSwitchLayout) {
+
+ let contextLayout = meetingLayout;
+ if (isMobile()) {
+ contextLayout = meetingLayout === 'custom' ? 'smart' : meetingLayout;
+ }
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_LAYOUT_TYPE,
+ value: contextLayout,
+ });
+
+ updateSettings({
+ application: {
+ ...Settings.application,
+ selectedLayout: contextLayout,
+ },
+ });
+ }
+
+ if (pushLayoutMeetingDidChange) {
+ updateSettings({
+ application: {
+ ...Settings.application,
+ pushLayout: pushLayoutMeeting,
+ },
+ });
+ }
+
+ if (meetingLayout === "custom" && selectedLayout === "custom" && !isPresenter) {
+
+ if (meetingLayoutFocusedCamera !== prevProps.meetingLayoutFocusedCamera
+ || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) {
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_FOCUSED_CAMERA_ID,
+ value: meetingLayoutFocusedCamera,
+ });
+ }
+
+ if (meetingLayoutCameraPosition !== prevProps.meetingLayoutCameraPosition
+ || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) {
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAMERA_DOCK_POSITION,
+ value: meetingLayoutCameraPosition,
+ });
+ }
+
+ if (!equalDouble(meetingLayoutVideoRate, prevProps.meetingLayoutVideoRate)
+ || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) {
+
+ let w, h;
+ if (horizontalPosition) {
+ w = window.innerWidth * meetingLayoutVideoRate;
+ h = cameraHeight;
+ } else {
+ w = cameraWidth;
+ h = window.innerHeight * meetingLayoutVideoRate;
+ }
+
+ if (isMeetingLayoutResizing !== prevProps.isMeetingLayoutResizing) {
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAMERA_DOCK_IS_RESIZING,
+ value: isMeetingLayoutResizing,
+ });
+ }
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_CAMERA_DOCK_SIZE,
+ value: {
+ width: w,
+ height: h,
+ browserWidth: window.innerWidth,
+ browserHeight: window.innerHeight,
+ }
+ });
+ }
+
+ if (meetingPresentationIsOpen !== prevProps.meetingPresentationIsOpen
+ || meetingLayoutUpdatedAt !== prevProps.meetingLayoutUpdatedAt) {
+
+ layoutContextDispatch({
+ type: ACTIONS.SET_PRESENTATION_IS_OPEN,
+ value: meetingPresentationIsOpen,
+ });
+ }
+ }
+
+ const layoutChanged = presentationIsOpen !== prevProps.presentationIsOpen
+ || selectedLayout !== prevProps.selectedLayout
+ || cameraIsResizing !== prevProps.cameraIsResizing
+ || cameraPosition !== prevProps.cameraPosition
+ || focusedCamera !== prevProps.focusedCamera
+ || !equalDouble(presentationVideoRate, prevProps.presentationVideoRate);
+
+ if (pushLayout !== prevProps.pushLayout) { // push layout once after presenter toggles / special case where we set pushLayout to false in all viewers
+ if (isModerator) {
+ setPushLayout(pushLayout);
+ }
+ }
+
+ if (pushLayout && layoutChanged || pushLayout !== prevProps.pushLayout) { // change layout sizes / states
+ if (isPresenter) {
+ setMeetingLayout();
+ }
+ }
+
+ if (selectedLayout !== prevProps.selectedLayout) {
+ Session.set('isGridEnabled', selectedLayout === LAYOUT_TYPE.VIDEO_FOCUS);
+ }
+ }
+
+ render() {
+ return null;
+ }
+};
+
+PushLayoutEngine.propTypes = propTypes;
+
+export default PushLayoutEngine;
diff --git a/src/2.7.12/imports/ui/components/layout/service.js b/src/2.7.12/imports/ui/components/layout/service.js
new file mode 100644
index 00000000..ebb19878
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/service.js
@@ -0,0 +1,14 @@
+import { makeCall } from '/imports/ui/services/api';
+
+const setPushLayout = (pushLayout) => {
+ makeCall('setPushLayout', { pushLayout });
+};
+
+const setMeetingLayout = (options) => {
+ makeCall('changeLayout', options);
+};
+
+export default {
+ setPushLayout,
+ setMeetingLayout,
+};
diff --git a/src/2.7.12/imports/ui/components/layout/utils.js b/src/2.7.12/imports/ui/components/layout/utils.js
new file mode 100644
index 00000000..26c8c98a
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/layout/utils.js
@@ -0,0 +1,69 @@
+import { DEVICE_TYPE, LAYOUT_TYPE } from './enums';
+
+const phoneUpperBoundary = 600;
+const tabletPortraitUpperBoundary = 900;
+const tabletLandscapeUpperBoundary = 1200;
+
+const windowSize = () => window.document.documentElement.clientWidth;
+const isMobile = () => windowSize() <= (phoneUpperBoundary - 1);
+const isTabletPortrait = () => windowSize() >= phoneUpperBoundary
+ && windowSize() <= (tabletPortraitUpperBoundary - 1);
+const isTabletLandscape = () => windowSize() >= tabletPortraitUpperBoundary
+ && windowSize() <= (tabletLandscapeUpperBoundary - 1);
+const isTablet = () => windowSize() >= phoneUpperBoundary
+ && windowSize() <= (tabletLandscapeUpperBoundary - 1);
+const isDesktop = () => windowSize() >= tabletLandscapeUpperBoundary;
+
+const device = {
+ isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop,
+};
+
+export default device;
+export {
+ isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop,
+};
+
+// Array for select component to select diferent layout
+const suportedLayouts = [
+ {
+ layoutKey: LAYOUT_TYPE.SMART_LAYOUT,
+ layoutName: 'Smart Layout',
+ suportedDevices: [
+ DEVICE_TYPE.MOBILE,
+ DEVICE_TYPE.TABLET,
+ DEVICE_TYPE.TABLET_PORTRAIT,
+ DEVICE_TYPE.TABLET_LANDSCAPE,
+ DEVICE_TYPE.DESKTOP,
+ ],
+ },
+ {
+ layoutKey: LAYOUT_TYPE.VIDEO_FOCUS,
+ layoutName: 'Video Focus',
+ suportedDevices: [
+ DEVICE_TYPE.MOBILE,
+ DEVICE_TYPE.TABLET,
+ DEVICE_TYPE.TABLET_PORTRAIT,
+ DEVICE_TYPE.TABLET_LANDSCAPE,
+ DEVICE_TYPE.DESKTOP,
+ ],
+ },
+ {
+ layoutKey: LAYOUT_TYPE.PRESENTATION_FOCUS,
+ layoutName: 'Presentation Focus',
+ suportedDevices: [
+ DEVICE_TYPE.MOBILE,
+ DEVICE_TYPE.TABLET,
+ DEVICE_TYPE.TABLET_PORTRAIT,
+ DEVICE_TYPE.TABLET_LANDSCAPE,
+ DEVICE_TYPE.DESKTOP,
+ ],
+ },
+ {
+ layoutKey: LAYOUT_TYPE.CUSTOM_LAYOUT,
+ layoutName: 'Custom Layout',
+ suportedDevices: [
+ DEVICE_TYPE.DESKTOP,
+ ],
+ },
+];
+export { suportedLayouts };
diff --git a/src/2.7.12/imports/ui/components/learning-dashboard/service.js b/src/2.7.12/imports/ui/components/learning-dashboard/service.js
new file mode 100644
index 00000000..8839ddf2
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/learning-dashboard/service.js
@@ -0,0 +1,56 @@
+import Users from '/imports/api/users';
+import Auth from '/imports/ui/services/auth';
+import Meetings from '/imports/api/meetings';
+
+const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
+
+const isModerator = () => {
+ const user = Users.findOne(
+ {
+ meetingId: Auth.meetingID,
+ userId: Auth.userID,
+ },
+ { fields: { role: 1 } },
+ );
+
+ if (user && user.role === ROLE_MODERATOR) {
+ return true;
+ }
+
+ return false;
+};
+
+const getLearningDashboardAccessToken = () => ((
+ Meetings.findOne(
+ { meetingId: Auth.meetingID, learningDashboardAccessToken: { $exists: true } },
+ {
+ fields: { learningDashboardAccessToken: 1 },
+ },
+ ) || {}).learningDashboardAccessToken || null);
+
+const setLearningDashboardCookie = () => {
+ const learningDashboardAccessToken = getLearningDashboardAccessToken();
+ if (learningDashboardAccessToken !== null) {
+ const lifetime = new Date();
+ lifetime.setTime(lifetime.getTime() + (3600000)); // 1h (extends 7d when open Dashboard)
+ document.cookie = `ld-${Auth.meetingID}=${getLearningDashboardAccessToken()}; expires=${lifetime.toGMTString()}; path=/`;
+ return true;
+ }
+ return false;
+};
+
+const openLearningDashboardUrl = (lang) => {
+ const APP = Meteor.settings.public.app;
+ if (getLearningDashboardAccessToken() && setLearningDashboardCookie()) {
+ window.open(`${APP.learningDashboardBase}/?meeting=${Auth.meetingID}&lang=${lang}`, '_blank');
+ } else {
+ window.open(`${APP.learningDashboardBase}/?meeting=${Auth.meetingID}&sessionToken=${Auth.sessionToken}&lang=${lang}`, '_blank');
+ }
+};
+
+export default {
+ isModerator,
+ getLearningDashboardAccessToken,
+ setLearningDashboardCookie,
+ openLearningDashboardUrl,
+};
diff --git a/src/2.7.12/imports/ui/components/legacy/component.jsx b/src/2.7.12/imports/ui/components/legacy/component.jsx
new file mode 100644
index 00000000..5a33ce94
--- /dev/null
+++ b/src/2.7.12/imports/ui/components/legacy/component.jsx
@@ -0,0 +1,205 @@
+import React, { Component } from 'react';
+import { IntlProvider, FormattedMessage } from 'react-intl';
+import browserInfo from '/imports/utils/browserInfo';
+import deviceInfo from '/imports/utils/deviceInfo';
+import './styles.css';
+
+
+// currently supported locales.
+// import ar from 'react-intl/locale-data/ar';
+// import bg from 'react-intl/locale-data/bg';
+// import cs from 'react-intl/locale-data/cs';
+// import de from 'react-intl/locale-data/de';
+// import el from 'react-intl/locale-data/el';
+// import en from 'react-intl/locale-data/en';
+// import es from 'react-intl/locale-data/es';
+// import eu from 'react-intl/locale-data/eu';
+// import fa from 'react-intl/locale-data/fa';
+// import fi from 'react-intl/locale-data/fi';
+// import fr from 'react-intl/locale-data/fr';
+// import he from 'react-intl/locale-data/he';
+// import hi from 'react-intl/locale-data/hi';
+// import hu from 'react-intl/locale-data/hu';
+// import id from 'react-intl/locale-data/id';
+// import it from 'react-intl/locale-data/it';
+// import ja from 'react-intl/locale-data/ja';
+// import km from 'react-intl/locale-data/km';
+// import pl from 'react-intl/locale-data/pl';
+// import pt from 'react-intl/locale-data/pt';
+// import ru from 'react-intl/locale-data/ru';
+// import sv from 'react-intl/locale-data/sv';
+// import tr from 'react-intl/locale-data/tr';
+// import uk from 'react-intl/locale-data/uk';
+// import vi from 'react-intl/locale-data/vi';
+// import zh from 'react-intl/locale-data/zh';
+
+// // This class is the only component loaded on legacy (unsupported) browsers.
+// // What is included here needs to be minimal and carefully considered because some
+// // things can't be polyfilled.
+
+// addLocaleData([
+// ...ar,
+// ...bg,
+// ...cs,
+// ...de,
+// ...el,
+// ...en,
+// ...es,
+// ...eu,
+// ...fa,
+// ...fi,
+// ...fr,
+// ...he,
+// ...hi,
+// ...hu,
+// ...id,
+// ...it,
+// ...ja,
+// ...km,
+// ...pl,
+// ...pt,
+// ...ru,
+// ...sv,
+// ...tr,
+// ...uk,
+// ...vi,
+// ...zh,
+// ]);
+
+const FETCHING = 'fetching';
+const FALLBACK = 'fallback';
+const READY = 'ready';
+const supportedBrowsers = ['Chrome', 'Firefox', 'Safari', 'Opera', 'Microsoft Edge', 'Yandex Browser'];
+const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
+const CLIENT_VERSION = Meteor.settings.public.app.html5ClientBuild;
+
+export default class Legacy extends Component {
+ constructor(props) {
+ super(props);
+
+ const locale = navigator.languages ? navigator.languages[0] : false
+ || navigator.language;
+
+ const url = `./locale?locale=${locale}`;
+ const localesPath = 'locales';
+
+ const that = this;
+ this.state = { viewState: FETCHING };
+ fetch(url)
+ .then((response) => {
+ if (!response.ok) {
+ return Promise.reject();
+ }
+
+ return response.json();
+ })
+ .then(({ normalizedLocale, regionDefaultLocale }) => {
+ fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json?v=${CLIENT_VERSION}`)
+ .then((response) => {
+ if (!response.ok) {
+ return Promise.reject();
+ }
+ return response.json();
+ })
+ .then((messages) => {
+ if (regionDefaultLocale !== '') {
+ fetch(`${localesPath}/${regionDefaultLocale}.json?v=${CLIENT_VERSION}`)
+ .then((response) => {
+ if (!response.ok) {
+ return Promise.resolve();
+ }
+ return response.json();
+ })
+ .then((regionDefaultMessages) => {
+ messages = Object.assign(messages, regionDefaultMessages);
+ this.setState({ messages});
+ });
+ }
+
+ if (normalizedLocale && normalizedLocale !== DEFAULT_LANGUAGE && normalizedLocale !== regionDefaultLocale) {
+ fetch(`${localesPath}/${normalizedLocale}.json?v=${CLIENT_VERSION}`)
+ .then((response) => {
+ if (!response.ok) {
+ return Promise.reject();
+ }
+ return response.json();
+ })
+ .then((localeMessages) => {
+ messages = Object.assign(messages, localeMessages);
+ this.setState({ messages});
+ })
+ .catch(() => {
+ normalizedLocale = (regionDefaultLocale) || DEFAULT_LANGUAGE;
+ const dasherizedLocale = normalizedLocale.replace('_', '-');
+ this.setState({ messages, normalizedLocale: dasherizedLocale, viewState: READY });
+ });
+ }
+ return messages;
+ })
+ .then((messages) => {
+ const dasherizedLocale = normalizedLocale.replace('_', '-');
+ this.setState({ messages, normalizedLocale: dasherizedLocale, viewState: READY });
+ })
+ .catch(() => {
+ that.setState({ viewState: FALLBACK });
+ });
+ })
+ .catch(() => {
+ that.setState({ viewState: FALLBACK });
+ });
+ }
+
+ render() {
+ const { browserName, isSafari } = browserInfo;
+ const { isIos } = deviceInfo;
+
+ const { messages, normalizedLocale, viewState } = this.state;
+ const isSupportedBrowser = supportedBrowsers.includes(browserName);
+ const isUnsupportedIos = isIos && !isSafari;
+
+ let messageId = isSupportedBrowser ? 'app.legacy.upgradeBrowser' : 'app.legacy.unsupportedBrowser';
+ if (isUnsupportedIos) messageId = 'app.legacy.criosBrowser';
+
+ switch (viewState) {
+ case READY:
+ return (
+
+
+ Chrome
,
+ 1: