Merge pull request #3159 from oswaldoacauan/feature-chat
Add Chat functionality to HTML5 client
This commit is contained in:
commit
0cf7d88154
@ -21,6 +21,16 @@ a {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0,0,0,0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* DEBUG ONLY
|
||||
* {
|
||||
|
@ -16,10 +16,12 @@ addLocaleData([...en, ...es, ...pt]);
|
||||
// Safari sends us en-us instead of en-US
|
||||
let locale = navigator.language.split('-');
|
||||
locale = locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : navigator.language;
|
||||
// locale = 'pt-BR'; // Set a locale for testing
|
||||
|
||||
// locale = 'pt-BR'; // Set a locale for testing
|
||||
|
||||
/* TODO: We should load the en first then merge with the en-US
|
||||
(eg: load country translation then region) */
|
||||
let messages = Locales[locale] || Locales['en'] || {};
|
||||
let messages = Locales[locale] || Locales.en || {};
|
||||
|
||||
// Helper to load javascript libraries from the BBB server
|
||||
function loadLib(libname, success, fail) {
|
||||
|
@ -5,6 +5,32 @@ import { translateHTML5ToFlash } from '/imports/startup/server/helpers';
|
||||
import { logger } from '/imports/startup/server/logger';
|
||||
import { redisConfig } from '/config';
|
||||
|
||||
import RegexWebUrl from '/imports/utils/regex-weburl';
|
||||
|
||||
const HTML_SAFE_MAP = {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
};
|
||||
|
||||
const parseMessage = (message) => {
|
||||
message = message || '';
|
||||
|
||||
message = message.trim();
|
||||
|
||||
// Replace <br/> with \n\r
|
||||
message = message.replace(/<br\s*[\/]?>/gi, '\n\r');
|
||||
|
||||
// Sanitize. See: http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/
|
||||
message = message.replace(/[<>'"]/g, c => HTML_SAFE_MAP[c]);
|
||||
|
||||
// Replace flash links to flash valid ones
|
||||
message = message.replace(RegexWebUrl, "<a href='event:$&'><u>$&</u></a>");
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
Meteor.methods({
|
||||
// meetingId: the id of the meeting
|
||||
// chatObject: the object including info on the chat message, including the text
|
||||
@ -17,6 +43,7 @@ Meteor.methods({
|
||||
const chatType = chatObject.chat_type;
|
||||
const recipient = chatObject.to_userid;
|
||||
let eventName = null;
|
||||
|
||||
const action = function () {
|
||||
if (chatType === 'PUBLIC_CHAT') {
|
||||
eventName = 'send_public_chat_message';
|
||||
@ -31,8 +58,9 @@ Meteor.methods({
|
||||
}
|
||||
};
|
||||
|
||||
chatObject.message = parseMessage(chatObject.message);
|
||||
|
||||
if (isAllowedTo(action(), credentials) && chatObject.from_userid === requesterUserId) {
|
||||
chatObject.message = translateHTML5ToFlash(chatObject.message);
|
||||
let message = {
|
||||
payload: {
|
||||
message: chatObject,
|
||||
|
@ -1,14 +1,31 @@
|
||||
import Chat from '/imports/api/chat';
|
||||
import { logger } from '/imports/startup/server/logger';
|
||||
|
||||
const BREAK_TAG = '<br/>';
|
||||
|
||||
const parseMessage = (message) => {
|
||||
message = message || '';
|
||||
|
||||
// Replace \r and \n to <br/>
|
||||
message = message.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + BREAK_TAG + '$2');
|
||||
|
||||
// Replace flash links to html valid ones
|
||||
message = message.split(`<a href='event:`).join(`<a target="_blank" href='`);
|
||||
message = message.split(`<a href="event:`).join(`<a target="_blank" href="`);
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
export function addChatToCollection(meetingId, messageObject) {
|
||||
let id;
|
||||
|
||||
// manually convert time from 1.408645053653E12 to 1408645053653 if necessary
|
||||
// (this is the time_from that the Flash client outputs)
|
||||
messageObject.from_time = messageObject.from_time.toString().split('.').join('').split('E')[0];
|
||||
|
||||
messageObject.message = parseMessage(messageObject.message);
|
||||
|
||||
if ((messageObject.from_userid != null) && (messageObject.to_userid != null)) {
|
||||
messageObject.message = translateFlashToHTML5(messageObject.message);
|
||||
return id = Chat.upsert({
|
||||
meetingId: meetingId,
|
||||
'message.message': messageObject.message,
|
||||
@ -42,11 +59,3 @@ export function addChatToCollection(meetingId, messageObject) {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// translate '<br/>' breakline character to '\r' carriage return character for HTML5
|
||||
const translateFlashToHTML5 = function (message) {
|
||||
let result;
|
||||
result = message;
|
||||
result = result.replace(new RegExp(BREAK_LINE, 'g'), CARRIAGE_RETURN);
|
||||
return result;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Users from '/imports/api/users';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import {getInStorage, setInStorage} from '/imports/ui/components/app/service';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import {callServer} from '/imports/ui/services/api';
|
||||
import {clientConfig} from '/config';
|
||||
import {createVertoUserName, joinVertoAudio} from '/imports/api/verto';
|
||||
@ -12,7 +12,7 @@ function getVoiceBridge() {
|
||||
}
|
||||
|
||||
function amIListenOnly() {
|
||||
const uid = getInStorage('userID');
|
||||
const uid = Auth.getUser();
|
||||
return Users.findOne({ userId: uid }).user.listenOnly;
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ function joinVoiceCall(options) {
|
||||
|
||||
window.BBB = {};
|
||||
window.BBB.getMyUserInfo = function (callback) {
|
||||
const uid = getInStorage('userID');
|
||||
const uid = Auth.getUser();
|
||||
const result = {
|
||||
myUserID: uid,
|
||||
myUsername: Users.findOne({ userId: uid }).user.name,
|
||||
|
@ -2,7 +2,22 @@ import Chat from '/imports/api/chat';
|
||||
import Users from '/imports/api/users';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import { logger } from '/imports/startup/server/logger';
|
||||
import {clientConfig} from '/config';
|
||||
import { clientConfig } from '/config';
|
||||
|
||||
const BREAK_TAG = '<br/>';
|
||||
|
||||
const parseMessage = (message) => {
|
||||
message = message || '';
|
||||
|
||||
// Replace \r and \n to <br/>
|
||||
message = message.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + BREAK_TAG + '$2');
|
||||
|
||||
// Replace flash links to html valid ones
|
||||
message = message.split(`<a href='event:`).join(`<a target="_blank" href='`);
|
||||
message = message.split(`<a href="event:`).join(`<a target="_blank" href="`);
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
export function userJoined(meetingId, user, callback) {
|
||||
let userObject, userId, welcomeMessage, meetingObject;
|
||||
@ -85,7 +100,7 @@ export function userJoined(meetingId, user, callback) {
|
||||
userId: userId,
|
||||
message: {
|
||||
chat_type: 'SYSTEM_MESSAGE',
|
||||
message: welcomeMessage,
|
||||
message: parseMessage(welcomeMessage),
|
||||
from_color: '0x3399FF',
|
||||
to_userid: userId,
|
||||
from_userid: 'SYSTEM_MESSAGE',
|
||||
|
@ -1,8 +1,9 @@
|
||||
import {clientConfig} from '/config';
|
||||
import {getVoiceBridge} from '/imports/api/phone';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { clientConfig } from '/config';
|
||||
import { getVoiceBridge } from '/imports/api/phone';
|
||||
|
||||
function createVertoUserName() {
|
||||
const uid = getInStorage('userID');
|
||||
const uid = Auth.getUser();
|
||||
const uName = Users.findOne({ userId: uid }).user.name;
|
||||
const conferenceUsername = 'FreeSWITCH User - ' + encodeURIComponent(uName);
|
||||
return conferenceUsername;
|
||||
|
@ -3,5 +3,7 @@
|
||||
"app.userlist.participantsTitle": "Participants",
|
||||
"app.userlist.messagesTitle": "Messages",
|
||||
"app.userlist.presenter": "Presenter",
|
||||
"app.userlist.you": "You"
|
||||
"app.userlist.you": "You",
|
||||
"app.chat.submitLabel": "Send Message",
|
||||
"app.chat.inputLabel": "Message input for chat {name}"
|
||||
}
|
||||
|
@ -3,5 +3,7 @@
|
||||
"app.userlist.participantsTitle": "Participantes",
|
||||
"app.userlist.messagesTitle": "Mensagens",
|
||||
"app.userlist.presenter": "Apresentador",
|
||||
"app.userlist.you": "Você"
|
||||
"app.userlist.you": "Você",
|
||||
"app.chat.submitLabel": "Enviar Mensagem",
|
||||
"app.chat.inputLabel": "Campo de mensagem para conversa {name}"
|
||||
}
|
||||
|
@ -6,8 +6,9 @@ import { createHistory } from 'history';
|
||||
// route components
|
||||
import AppContainer from '../../ui/components/app/container';
|
||||
import {setCredentials, subscribeForData} from '../../ui/components/app/service';
|
||||
|
||||
import ChatContainer from '../../ui/components/chat/container';
|
||||
import UserListContainer from '../../ui/components/user-list/container';
|
||||
import ChatContainer from '../../ui/components/chat/ChatContainer';
|
||||
|
||||
const browserHistory = useRouterHistory(createHistory)({
|
||||
basename: '/html5client',
|
||||
|
@ -1,26 +1,17 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
|
||||
import Users from '/imports/api/users';
|
||||
import Chat from '/imports/api/chat';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Cursor from '/imports/api/cursor';
|
||||
import Polls from '/imports/api/polls';
|
||||
|
||||
function setInStorage(key, value) {
|
||||
if (!!value) {
|
||||
console.log('in setInStorage', key, value);
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
function getInStorage(key) {
|
||||
return localStorage.getItem(key);
|
||||
};
|
||||
|
||||
function setCredentials(nextState, replace) {
|
||||
if (!!nextState && !!nextState.params) {
|
||||
setInStorage('meetingID', nextState.params.meetingID);
|
||||
setInStorage('userID', nextState.params.userID);
|
||||
setInStorage('authToken', nextState.params.authToken);
|
||||
if (nextState && nextState.params.authToken) {
|
||||
const { meetingID, userID, authToken } = nextState.params;
|
||||
Auth.setCredentials(meetingID, userID, authToken);
|
||||
}
|
||||
};
|
||||
|
||||
@ -47,11 +38,7 @@ function subscribeForData() {
|
||||
};
|
||||
|
||||
function subscribeFor(collectionName) {
|
||||
const credentials = {
|
||||
meetingId: getInStorage('meetingID'),
|
||||
requesterUserId: getInStorage('userID'),
|
||||
requesterToken: getInStorage('authToken'),
|
||||
};
|
||||
const credentials = Auth.getCredentials();
|
||||
|
||||
// console.log("subscribingForData", collectionName, meetingID, userID, authToken);
|
||||
|
||||
@ -74,5 +61,4 @@ export {
|
||||
pollExists,
|
||||
subscribeForData,
|
||||
setCredentials,
|
||||
getInStorage,
|
||||
};
|
||||
|
@ -126,12 +126,12 @@ $actionsbar-height: 50px; // TODO: Change to ActionsBar real height
|
||||
}
|
||||
|
||||
@include mq($medium-up) {
|
||||
flex: 0 20vw;
|
||||
flex: 0 25vw;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
@include mq($xlarge-up) {
|
||||
flex-basis: 15vw;
|
||||
flex-basis: 20vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,19 +16,19 @@ $btn-danger-color: $color-white;
|
||||
$btn-danger-bg: $color-danger;
|
||||
$btn-danger-border: $color-danger;
|
||||
|
||||
$btn-border-size: 2px;
|
||||
$btn-border-radius: .2rem;
|
||||
$btn-border-size: $border-size;
|
||||
$btn-border-radius: $border-radius;
|
||||
$btn-font-weight: 600;
|
||||
$btn-spacing: .35rem;
|
||||
|
||||
$btn-sm-font-size: $font-size-small * .85;
|
||||
$btn-sm-padding: .25rem .75rem;
|
||||
$btn-sm-padding: $sm-padding-y $sm-padding-x;
|
||||
|
||||
$btn-md-font-size: $font-size-base * .85;
|
||||
$btn-md-padding: .375rem 1rem;
|
||||
$btn-md-padding: $md-padding-y $md-padding-x;
|
||||
|
||||
$btn-lg-font-size: $font-size-large * .85;
|
||||
$btn-lg-padding: .5rem 1.25rem;
|
||||
$btn-lg-padding: $lg-padding-y $lg-padding-x;
|
||||
|
||||
/* Base
|
||||
* ==========
|
||||
|
@ -1,12 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
export default class Chat extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
You are chatting with {this.props.currentChat}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
|
||||
import Chat from './Chat';
|
||||
|
||||
class ChatContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
currentChat: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const chatID = this.props.params.id || 'public';
|
||||
this.setState({ currentChat: chatID });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { chatID, ...props } = this.props.params;
|
||||
return (
|
||||
<Chat currentChat={chatID} {...props}>
|
||||
{this.props.children}
|
||||
</Chat>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default createContainer(() => {
|
||||
return {};
|
||||
}, ChatContainer);
|
46
bigbluebutton-html5/imports/ui/components/chat/component.jsx
Normal file
46
bigbluebutton-html5/imports/ui/components/chat/component.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import styles from './styles';
|
||||
|
||||
import MessageForm from './message-form/component';
|
||||
import MessageList from './message-list/component';
|
||||
import Icon from '../icon/component';
|
||||
|
||||
const ELEMENT_ID = 'chat-messages';
|
||||
|
||||
export default class Chat extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
messages,
|
||||
actions,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<section className={styles.chat}>
|
||||
<header className={styles.header}>
|
||||
<Link className={styles.closeChat} to="/users">
|
||||
<Icon iconName="left-arrow" /> {title}
|
||||
</Link>
|
||||
</header>
|
||||
<MessageList
|
||||
messages={messages}
|
||||
id={ELEMENT_ID}
|
||||
tabindex="0"
|
||||
role="log"
|
||||
aria-atomic="true"
|
||||
aria-relevant="additions"
|
||||
/>
|
||||
<MessageForm
|
||||
chatAreaId={ELEMENT_ID}
|
||||
chatTitle={title}
|
||||
handleSendMessage={actions.handleSendMessage}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
43
bigbluebutton-html5/imports/ui/components/chat/container.jsx
Normal file
43
bigbluebutton-html5/imports/ui/components/chat/container.jsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
|
||||
import Chat from './component';
|
||||
import ChatService from './service';
|
||||
|
||||
const PUBLIC_CHAT_KEY = 'public';
|
||||
|
||||
class ChatContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Chat {...this.props}>
|
||||
{this.props.children}
|
||||
</Chat>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default createContainer(({ params }) => {
|
||||
const chatID = params.chatID || PUBLIC_CHAT_KEY;
|
||||
|
||||
let messages = [];
|
||||
|
||||
if (chatID === PUBLIC_CHAT_KEY) {
|
||||
messages = ChatService.getPublicMessages();
|
||||
title = 'Public';
|
||||
} else {
|
||||
messages = ChatService.getPrivateMessages(chatID);
|
||||
title = ChatService.getChatTitle(chatID);
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
messages,
|
||||
actions: {
|
||||
handleSendMessage: message => ChatService.sendMessage(chatID, message),
|
||||
},
|
||||
};
|
||||
}, ChatContainer);
|
@ -0,0 +1,118 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import cx from 'classnames';
|
||||
import styles from './styles';
|
||||
|
||||
import Button from '../../button/component';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
|
||||
const propTypes = {
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
submitLabel: {
|
||||
id: 'app.chat.submitLabel',
|
||||
defaultMessage: 'Send Message',
|
||||
description: 'Chat submit button label',
|
||||
},
|
||||
inputLabel: {
|
||||
id: 'app.chat.inputLabel',
|
||||
defaultMessage: 'Message input for chat {name}',
|
||||
description: 'Chat message input label',
|
||||
},
|
||||
});
|
||||
|
||||
class MessageForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
message: '',
|
||||
};
|
||||
|
||||
this.handleMessageChange = this.handleMessageChange.bind(this);
|
||||
this.handleMessageKeyUp = this.handleMessageKeyUp.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleMessageKeyUp(e) {
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
this.refs.btnSubmit.click();
|
||||
|
||||
// FIX: I dont know why the live bellow dont trigger the handleSubmit function
|
||||
// this.refs.form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
handleMessageChange(e) {
|
||||
this.setState({ message: e.target.value });
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let message = this.state.message.trim();
|
||||
|
||||
// Sanitize. See: http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/
|
||||
|
||||
let div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(message));
|
||||
message = div.innerHTML;
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ message: '' });
|
||||
this.props.handleSendMessage(message);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, chatTitle } = this.props;
|
||||
|
||||
return (
|
||||
<form
|
||||
{...this.props}
|
||||
ref="form"
|
||||
className={cx(this.props.className, styles.form)}
|
||||
onSubmit={this.handleSubmit}>
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
onClick={() => alert('Not supported yet...')}
|
||||
icon={'circle-add'}
|
||||
size={'sm'}
|
||||
circle={true}
|
||||
/>
|
||||
</div>
|
||||
<TextareaAutosize
|
||||
className={styles.input}
|
||||
id="message-input"
|
||||
maxlength=""
|
||||
aria-controls={this.props.chatAreaId}
|
||||
aria-label={ intl.formatMessage(messages.inputLabel, { name: chatTitle }) }
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
spellcheck="true"
|
||||
value={this.state.message}
|
||||
onChange={this.handleMessageChange}
|
||||
onKeyUp={this.handleMessageKeyUp}
|
||||
/>
|
||||
<input
|
||||
ref="btnSubmit"
|
||||
className={'sr-only'}
|
||||
type="submit"
|
||||
value={ intl.formatMessage(messages.submitLabel) }
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageForm.propTypes = propTypes;
|
||||
MessageForm.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(MessageForm);
|
@ -0,0 +1,43 @@
|
||||
@import "imports/ui/stylesheets/variables/_all";
|
||||
|
||||
.form {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-end;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
border: $border-size solid $color-gray-lighter;
|
||||
background: #fff;
|
||||
border-radius: $border-radius 0 0 $border-radius;
|
||||
color: $color-gray-lighter;
|
||||
padding: $sm-padding-y $sm-padding-x;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: $border-size solid $color-gray-lighter;
|
||||
border-left: none;
|
||||
background-clip: padding-box;
|
||||
margin: 0;
|
||||
color: $color-gray;
|
||||
-webkit-appearance: none;
|
||||
box-shadow: none;
|
||||
outline: 0;
|
||||
padding: $sm-padding-y $sm-padding-x;
|
||||
resize: none;
|
||||
transition: none;
|
||||
border-radius: 0 $border-radius $border-radius 0;
|
||||
font-size: $font-size-base * .90;
|
||||
min-height: 4rem;
|
||||
max-height: 10rem;
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import styles from './styles';
|
||||
|
||||
import MessageListItem from './message-list-item/component';
|
||||
|
||||
const propTypes = {
|
||||
messages: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default class MessageList extends Component {
|
||||
_scrollBottom() {
|
||||
const node = findDOMNode(this);
|
||||
node.scrollTop = node.scrollHeight;
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
const node = findDOMNode(this);
|
||||
this.shouldScrollBottom = node.scrollTop + node.offsetHeight === node.scrollHeight;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.shouldScrollBottom) {
|
||||
this._scrollBottom();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._scrollBottom();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { messages } = this.props;
|
||||
return (
|
||||
<div {...this.props} className={styles.messageList}>
|
||||
{messages.map((message, index) => (
|
||||
<MessageListItem
|
||||
className={styles.messageListItem}
|
||||
key={index}
|
||||
message={message.content}
|
||||
user={message.sender}
|
||||
time={message.time}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageList.propTypes = propTypes;
|
@ -0,0 +1,89 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedTime } from 'react-intl';
|
||||
import cx from 'classnames';
|
||||
|
||||
import UserAvatar from '../../../user-avatar/component';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const propTypes = {
|
||||
user: React.PropTypes.shape({
|
||||
name: React.PropTypes.string.isRequired,
|
||||
isPresenter: React.PropTypes.bool.isRequired,
|
||||
isVoiceUser: React.PropTypes.bool.isRequired,
|
||||
isModerator: React.PropTypes.bool.isRequired,
|
||||
image: React.PropTypes.string,
|
||||
}),
|
||||
message: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.arrayOf(React.PropTypes.string),
|
||||
]).isRequired,
|
||||
time: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
};
|
||||
|
||||
export default class MessageListItem extends Component {
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
message,
|
||||
time,
|
||||
} = this.props;
|
||||
|
||||
const dateTime = new Date(time);
|
||||
|
||||
if (!user) {
|
||||
return this.renderSystemMessage();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<div className={styles.avatar}>
|
||||
<UserAvatar user={user} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.meta}>
|
||||
<div className={styles.name}>
|
||||
<span>{user.name}</span>
|
||||
</div>
|
||||
<time className={styles.time} datetime={dateTime}>
|
||||
<FormattedTime value={dateTime}/>
|
||||
</time>
|
||||
</div>
|
||||
{message.map((text, i) => (
|
||||
<p
|
||||
className={styles.message}
|
||||
key={i}
|
||||
dangerouslySetInnerHTML={ { __html: text } } >
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderSystemMessage() {
|
||||
const {
|
||||
message,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={cx(styles.item, styles.systemMessage)}>
|
||||
<div className={styles.content}>
|
||||
{message.map((text, i) => (
|
||||
<p
|
||||
className={styles.message}
|
||||
key={i}
|
||||
dangerouslySetInnerHTML={ { __html: text } } >
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageListItem.propTypes = propTypes;
|
||||
MessageListItem.defaultProps = defaultProps;
|
@ -0,0 +1,74 @@
|
||||
@import "imports/ui/stylesheets/variables/_all";
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex: 1;
|
||||
margin-bottom: $line-height-computed;
|
||||
font-size: $font-size-base * .90;
|
||||
}
|
||||
|
||||
.systemMessage {
|
||||
font-weight: 600;
|
||||
|
||||
.item + &,
|
||||
& + .item {
|
||||
margin-bottom: $line-height-computed * 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-basis: 1.7rem;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
overflow: hidden;
|
||||
margin-left: $line-height-computed / 2;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row;
|
||||
|
||||
& + .message {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
color: $color-gray;
|
||||
|
||||
> span {
|
||||
@extend %text-elipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
flex-basis: 3.5rem;
|
||||
color: $color-gray-light;
|
||||
text-transform: uppercase;
|
||||
font-size: 75%;
|
||||
margin-left: $line-height-computed / 2;
|
||||
|
||||
> span {
|
||||
vertical-align: sub;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
flex: 1;
|
||||
margin-top: $line-height-computed / 2;
|
||||
margin-bottom: 0;
|
||||
color: $color-text;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
@import "imports/ui/stylesheets/variables/_all";
|
||||
@import "imports/ui/stylesheets/mixins/_scrollable";
|
||||
|
||||
.messageList {
|
||||
@include scrollbox-vertical();
|
||||
flex-flow: column;
|
||||
flex-grow: 1;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-right: -$line-height-computed;
|
||||
padding-right: $line-height-computed;
|
||||
padding-bottom: $line-height-computed;
|
||||
}
|
144
bigbluebutton-html5/imports/ui/components/chat/service.js
Normal file
144
bigbluebutton-html5/imports/ui/components/chat/service.js
Normal file
@ -0,0 +1,144 @@
|
||||
import Chats from '/imports/api/chat';
|
||||
import Users from '/imports/api/users';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
|
||||
import { callServer } from '/imports/ui/services/api';
|
||||
|
||||
const GROUPING_MESSAGES_WINDOW = 60000;
|
||||
|
||||
const SYSTEM_CHAT_TYPE = 'SYSTEM_MESSAGE';
|
||||
const PUBLIC_CHAT_TYPE = 'PUBLIC_CHAT';
|
||||
const PRIVATE_CHAT_TYPE = 'PRIVATE_CHAT';
|
||||
|
||||
const PUBLIC_CHAT_ID = 'public';
|
||||
|
||||
/* TODO: Same map is done in the user-list/service we should share this someway */
|
||||
|
||||
const mapUser = (user) => ({
|
||||
id: user.userid,
|
||||
name: user.name,
|
||||
isPresenter: user.presenter,
|
||||
isModerator: user.role === 'MODERATOR',
|
||||
isCurrent: user.userid === Auth.getUser(),
|
||||
isVoiceUser: user.voiceUser.joined,
|
||||
isMuted: user.voiceUser.muted,
|
||||
isListenOnly: user.listenOnly,
|
||||
isSharingWebcam: user.webcam_stream.length,
|
||||
});
|
||||
|
||||
const mapMessage = (message) => {
|
||||
let mappedMessage = {
|
||||
content: [message.message],
|
||||
time: +(message.from_time), //+ message.from_tz_offset,
|
||||
sender: null,
|
||||
};
|
||||
|
||||
if (message.chat_type !== SYSTEM_CHAT_TYPE) {
|
||||
mappedMessage.sender = getUser(message.from_userid);
|
||||
}
|
||||
|
||||
return mappedMessage;
|
||||
};
|
||||
|
||||
const reduceMessages = (previous, current, index, array) => {
|
||||
let lastMessage = previous[previous.length - 1];
|
||||
|
||||
if (!lastMessage || !lastMessage.sender || !current.sender) { // Skip system messages
|
||||
return previous.concat(current);
|
||||
}
|
||||
|
||||
// Check if the last message is from the same user and time discrepancy
|
||||
// between the two messages exceeds window and then group current message
|
||||
// with the last one
|
||||
|
||||
if (lastMessage.sender.id === current.sender.id
|
||||
&& (current.time - lastMessage.time) <= GROUPING_MESSAGES_WINDOW) {
|
||||
lastMessage.content = lastMessage.content.concat(current.content);
|
||||
return previous;
|
||||
} else {
|
||||
return previous.concat(current);
|
||||
}
|
||||
};
|
||||
|
||||
const getUser = (userID) => {
|
||||
const user = Users.findOne({ userId: userID });
|
||||
if (user) {
|
||||
return mapUser(user.user);
|
||||
} else {
|
||||
throw `User ${userID} not found`;
|
||||
}
|
||||
};
|
||||
|
||||
const getPublicMessages = () => {
|
||||
let publicMessages = Chats.find({
|
||||
'message.chat_type': { $in: [PUBLIC_CHAT_TYPE, SYSTEM_CHAT_TYPE] },
|
||||
}, {
|
||||
sort: ['message.from_time'],
|
||||
})
|
||||
.fetch();
|
||||
|
||||
let systemMessage = Chats.findOne({ 'message.chat_type': SYSTEM_CHAT_TYPE });
|
||||
|
||||
return publicMessages
|
||||
.map(m => m.message)
|
||||
.map(mapMessage)
|
||||
.reduce(reduceMessages, []);
|
||||
};
|
||||
|
||||
const getPrivateMessages = (userID) => {
|
||||
let messages = Chats.find({
|
||||
'message.chat_type': PRIVATE_CHAT_TYPE,
|
||||
$or: [
|
||||
{ 'message.to_userid': userID },
|
||||
{ 'message.from_userid': userID },
|
||||
],
|
||||
}, {
|
||||
sort: ['message.from_time'],
|
||||
}).fetch();
|
||||
|
||||
return messages
|
||||
.map(m => m.message)
|
||||
.map(mapMessage)
|
||||
.reduce(reduceMessages, []);
|
||||
};
|
||||
|
||||
const getChatTitle = (userID) => {
|
||||
const user = getUser(userID);
|
||||
return user.name;
|
||||
};
|
||||
|
||||
const sendMessage = (receiverID, message) => {
|
||||
const isPublic = receiverID === PUBLIC_CHAT_ID;
|
||||
|
||||
const sender = getUser(Auth.getUser());
|
||||
const receiver = !isPublic ? getUser(receiverID) : {
|
||||
id: 'public_chat_userid',
|
||||
name: 'public_chat_username',
|
||||
};
|
||||
|
||||
/* FIX: Why we need all this payload to send a message?
|
||||
* The server only really needs the message, from_userid, to_userid and from_lang
|
||||
*/
|
||||
|
||||
let messagePayload = {
|
||||
message: message,
|
||||
chat_type: isPublic ? PUBLIC_CHAT_TYPE : PRIVATE_CHAT_TYPE,
|
||||
from_userid: sender.id,
|
||||
from_username: sender.name,
|
||||
from_tz_offset: (new Date()).getTimezoneOffset(),
|
||||
to_username: receiver.name,
|
||||
to_userid: receiver.id,
|
||||
from_lang: window.navigator.userLanguage || window.navigator.language,
|
||||
from_time: Date.now(),
|
||||
from_color: 0,
|
||||
};
|
||||
|
||||
return callServer('sendChatMessagetoServer', messagePayload);
|
||||
};
|
||||
|
||||
export default {
|
||||
getPublicMessages,
|
||||
getPrivateMessages,
|
||||
getChatTitle,
|
||||
sendMessage,
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
@import "imports/ui/stylesheets/variables/_all";
|
||||
|
||||
.chat {
|
||||
background-color: #fff;
|
||||
margin: $line-height-computed;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.closeChat {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: $line-height-computed;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import Deskshare from '/imports/api/deskshare';
|
||||
import {conferenceUsername, joinVertoAudio, watchVertoVideo} from '/imports/api/verto';
|
||||
import {getInStorage} from '/imports/ui/components/app/service';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import {getVoiceBridge} from '/imports/api/phone';
|
||||
|
||||
// when the meeting information has been updated check to see if it was
|
||||
@ -16,7 +16,7 @@ function videoIsBroadcasting() {
|
||||
|
||||
if (ds.broadcasting) {
|
||||
console.log('Deskshare is now broadcasting');
|
||||
if (ds.startedBy != getInStorage('userID')) {
|
||||
if (ds.startedBy != Auth.getUser()) {
|
||||
console.log('deskshare wasn\'t initiated by me');
|
||||
presenterDeskshareHasStarted();
|
||||
return true;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Polls } from '/imports/api/polls';
|
||||
import {callServer} from '/imports/ui/services/api';
|
||||
import { callServer } from '/imports/ui/services/api';
|
||||
|
||||
let mapPolls = function () {
|
||||
let poll = Polls.findOne({});
|
||||
|
@ -2,6 +2,19 @@ import React, { Component, PropTypes } from 'react';
|
||||
import styles from './styles.scss';
|
||||
import cx from 'classnames';
|
||||
|
||||
const propTypes = {
|
||||
user: React.PropTypes.shape({
|
||||
name: React.PropTypes.string.isRequired,
|
||||
isPresenter: React.PropTypes.bool.isRequired,
|
||||
isVoiceUser: React.PropTypes.bool.isRequired,
|
||||
isModerator: React.PropTypes.bool.isRequired,
|
||||
image: React.PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
};
|
||||
|
||||
export default class UserAvatar extends Component {
|
||||
render() {
|
||||
let user = this.props.user;
|
||||
@ -10,14 +23,18 @@ export default class UserAvatar extends Component {
|
||||
avatarClasses[styles.presenter] = user.isPresenter;
|
||||
avatarClasses[styles.voiceUser] = user.isVoiceUser;
|
||||
avatarClasses[styles.moderator] = user.isModerator;
|
||||
|
||||
// avatarClasses[styles.image] = user.image;
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className={cx(avatarClasses, styles.userAvatar)}>
|
||||
<span>
|
||||
{user.name.slice(0, 1)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserAvatar.propTypes = propTypes;
|
||||
UserAvatar.defaultProps = defaultProps;
|
||||
|
@ -5,9 +5,8 @@ import classNames from 'classnames';
|
||||
|
||||
export default class ChatListItem extends Component {
|
||||
render() {
|
||||
|
||||
return (
|
||||
<li tabIndex='0' className={styles.chatListItem}>
|
||||
<li tabIndex='0' className={styles.chatListItem} {...this.props}>
|
||||
<div className={styles.chatThumbnail}>
|
||||
<i className="icon-bbb-group-chat"></i>
|
||||
</div>
|
||||
|
@ -1,11 +1,28 @@
|
||||
import React, { Component } from 'react';
|
||||
import styles from './styles.scss';
|
||||
import { Link } from 'react-router';
|
||||
import UserListItem from './user-list-item/component.jsx';
|
||||
import ChatListItem from './chat-list-item/component.jsx';
|
||||
import { withRouter } from 'react-router';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class UserList extends Component {
|
||||
import UserListItem from './user-list-item/component.jsx';
|
||||
import ChatListItem from './chat-list-item/component.jsx';
|
||||
|
||||
class UserList extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleMessageClick = this.handleMessageClick.bind(this);
|
||||
this.handleParticipantDblClick = this.handleParticipantDblClick.bind(this);
|
||||
}
|
||||
|
||||
handleMessageClick(chatID) {
|
||||
this.props.router.push(`/users/chat/${chatID}`);
|
||||
}
|
||||
|
||||
handleParticipantDblClick(userID) {
|
||||
this.props.router.push(`/users/chat/${userID}`);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.userList}>
|
||||
@ -26,7 +43,7 @@ export default class UserList extends Component {
|
||||
/>
|
||||
</h2>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
@ -49,10 +66,10 @@ export default class UserList extends Component {
|
||||
/>
|
||||
</h3>
|
||||
<ul className={styles.chatsList} tabIndex="1">
|
||||
<ChatListItem />
|
||||
<ChatListItem onClick={() => this.handleMessageClick('public')} />
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderParticipants() {
|
||||
@ -68,10 +85,20 @@ export default class UserList extends Component {
|
||||
</h3>
|
||||
<div className={styles.scrollableList}>
|
||||
<ul className={styles.participantsList} tabIndex="1">
|
||||
{this.props.users.map(user => <UserListItem accessibilityLabel={'Status abc'} accessible={true} key={user.id} user={user}/>)}
|
||||
{this.props.users.map(user => (
|
||||
<UserListItem
|
||||
onDoubleClick={() => this.handleParticipantDblClick(user.id)}
|
||||
accessibilityLabel={'Status abc'}
|
||||
accessible={true}
|
||||
key={user.id}
|
||||
user={user}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(UserList);
|
||||
|
@ -9,7 +9,7 @@ let cx = classNames.bind(styles);
|
||||
export default class ChatListItem extends Component {
|
||||
render() {
|
||||
return (
|
||||
<li tabIndex='0' className={styles.userListItem}>
|
||||
<li tabIndex='0' className={styles.userListItem} {...this.props}>
|
||||
<UserAvatar user={this.props.user}/>
|
||||
{this.renderUserName()}
|
||||
{this.renderUserIcons()}
|
||||
@ -21,7 +21,7 @@ export default class ChatListItem extends Component {
|
||||
let user = this.props.user;
|
||||
|
||||
let userNameSub = null;
|
||||
if(user.isPresenter) {
|
||||
if (user.isPresenter) {
|
||||
userNameSub = (
|
||||
<p className={styles.userNameSub}>
|
||||
<FormattedMessage
|
||||
@ -31,7 +31,7 @@ export default class ChatListItem extends Component {
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
} else if(user.isCurrent) {
|
||||
} else if (user.isCurrent) {
|
||||
userNameSub = (
|
||||
<p className={styles.userNameSub}>
|
||||
(<FormattedMessage
|
||||
@ -50,7 +50,7 @@ export default class ChatListItem extends Component {
|
||||
</h3>
|
||||
{userNameSub}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderUserIcons() {
|
||||
@ -58,7 +58,7 @@ export default class ChatListItem extends Component {
|
||||
|
||||
let audioChatIcon = null;
|
||||
if (user.isVoiceUser || user.isListenOnly) {
|
||||
if(user.isMuted) {
|
||||
if (user.isMuted) {
|
||||
audioChatIcon = 'icon-bbb-audio-off';
|
||||
} else {
|
||||
audioChatIcon = user.isListenOnly ? 'icon-bbb-listen' : 'icon-bbb-audio';
|
||||
@ -77,6 +77,6 @@ export default class ChatListItem extends Component {
|
||||
<i className='icon-bbb-more rotate-quarter'></i>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {getInStorage} from '/imports/ui/components/app/service';
|
||||
import { getCredentials } from '/imports/ui/services/auth';
|
||||
|
||||
function callServer(name) {
|
||||
if (!name || !(typeof (name) === 'string' || name instanceof String) || name.length === 0 ||
|
||||
@ -7,11 +7,7 @@ function callServer(name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
meetingId: getInStorage('meetingID'),
|
||||
requesterUserId: getInStorage('userID'),
|
||||
requesterToken: getInStorage('authToken'),
|
||||
};
|
||||
const credentials = getCredentials();
|
||||
|
||||
// slice off the first element. That is the function name but we already have that.
|
||||
const args = Array.prototype.slice.call(arguments, 1);
|
||||
|
27
bigbluebutton-html5/imports/ui/services/auth/index.js
Normal file
27
bigbluebutton-html5/imports/ui/services/auth/index.js
Normal file
@ -0,0 +1,27 @@
|
||||
import Storage from '/imports/ui/services/storage';
|
||||
|
||||
export const setCredentials = (meeting, user, token) => {
|
||||
Storage.set('meetingID', meeting);
|
||||
Storage.set('userID', user);
|
||||
Storage.set('authToken', token);
|
||||
};
|
||||
|
||||
export const getCredentials = () => ({
|
||||
meetingId: Storage.get('meetingID'),
|
||||
requesterUserId: Storage.get('userID'),
|
||||
requesterToken: Storage.get('authToken'),
|
||||
});
|
||||
|
||||
export const getMeeting = () => getCredentials().meetingId;
|
||||
|
||||
export const getUser = () => getCredentials().requesterUserId;
|
||||
|
||||
export const getToken = () => getCredentials().requesterToken;
|
||||
|
||||
export default {
|
||||
setCredentials,
|
||||
getCredentials,
|
||||
getMeeting,
|
||||
getUser,
|
||||
getToken,
|
||||
};
|
@ -1,15 +1,11 @@
|
||||
const STORAGE = localStorage;
|
||||
const PREFIX = 'bbb_';
|
||||
const PREFIX = 'BBB_';
|
||||
|
||||
function get(key) {
|
||||
return STORAGE.getItem(key);
|
||||
}
|
||||
const get = (key) => STORAGE.getItem(`${PREFIX}${key}`);
|
||||
|
||||
function set(key) {
|
||||
STORAGE.setItem(key);
|
||||
}
|
||||
const set = (key, value) => STORAGE.setItem(`${PREFIX}${key}`, value);
|
||||
|
||||
export default {
|
||||
get,
|
||||
set
|
||||
}
|
||||
set,
|
||||
};
|
||||
|
@ -0,0 +1,27 @@
|
||||
@mixin scrollbox-vertical($bg-color: white) {
|
||||
overflow-y: auto;
|
||||
background:
|
||||
/* Shadow covers */
|
||||
linear-gradient($bg-color 30%, rgba(255,255,255,0)),
|
||||
linear-gradient(rgba(255,255,255,0), $bg-color 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}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
@import "./general";
|
||||
@import "./breakpoints";
|
||||
@import "./palette";
|
||||
@import "./typography";
|
||||
|
@ -0,0 +1,11 @@
|
||||
$border-size: 2px;
|
||||
$border-radius: .2rem;
|
||||
|
||||
$sm-padding-x: .75rem;
|
||||
$sm-padding-y: .25rem;
|
||||
|
||||
$md-padding-x: 1rem;
|
||||
$md-padding-y: .375rem;
|
||||
|
||||
$lg-padding-x: 1.25rem;
|
||||
$lg-padding-y: .5rem;
|
@ -3,6 +3,7 @@ $color-white: #F3F6F9 !default;
|
||||
$color-gray: #353B42 !default;
|
||||
$color-gray-dark: #2A2D33 !default;
|
||||
$color-gray-light: #8B9AA8 !default;
|
||||
$color-gray-lighter: lighten($color-gray-light, 25%) !default;
|
||||
|
||||
$color-primary: #299AD5 !default;
|
||||
$color-success: #4DC0A2 !default;
|
||||
|
@ -15,3 +15,16 @@ $headings-font-family: inherit !default;
|
||||
$headings-font-weight: 500 !default;
|
||||
$headings-line-height: 1.1 !default;
|
||||
$headings-color: inherit !default;
|
||||
|
||||
/*
|
||||
* Placeholders
|
||||
* ===============
|
||||
*/
|
||||
|
||||
%text-elipsis {
|
||||
min-width: 0;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
110
bigbluebutton-html5/imports/utils/regex-weburl.js
Normal file
110
bigbluebutton-html5/imports/utils/regex-weburl.js
Normal file
@ -0,0 +1,110 @@
|
||||
//
|
||||
// Regular Expression for URL validation
|
||||
//
|
||||
// Author: Diego Perini
|
||||
// Updated: 2010/12/05
|
||||
// License: MIT
|
||||
//
|
||||
// Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
// the regular expression composed & commented
|
||||
// could be easily tweaked for RFC compliance,
|
||||
// it was expressly modified to fit & satisfy
|
||||
// these test for an URL shortener:
|
||||
//
|
||||
// http://mathiasbynens.be/demo/url-regex
|
||||
//
|
||||
// Notes on possible differences from a standard/generic validation:
|
||||
//
|
||||
// - utf-8 char class take in consideration the full Unicode range
|
||||
// - TLDs have been made mandatory so single names like "localhost" fails
|
||||
// - protocols have been restricted to ftp, http and https only as requested
|
||||
//
|
||||
// Changes:
|
||||
//
|
||||
// - IP address dotted notation validation, range: 1.0.0.0 - 223.255.255.255
|
||||
// first and last IP address of each class is considered invalid
|
||||
// (since they are broadcast/network addresses)
|
||||
//
|
||||
// - Added exclusion of private, reserved and/or local networks ranges
|
||||
//
|
||||
// - Made starting path slash optional (http://example.com?foo=bar)
|
||||
//
|
||||
// - Allow a dot (.) at the end of hostnames (http://example.com.)
|
||||
//
|
||||
// Compressed one-line versions:
|
||||
//
|
||||
// Javascript version
|
||||
//
|
||||
// /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
|
||||
//
|
||||
// PHP version
|
||||
//
|
||||
// _^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]-*)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]-*)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$_iuS
|
||||
//
|
||||
|
||||
export default new RegExp(
|
||||
|
||||
// protocol identifier
|
||||
'(?:(?:https?|ftp)://)' +
|
||||
|
||||
// user:pass authentication
|
||||
'(?:\\S+(?::\\S*)?@)?' +
|
||||
'(?:' +
|
||||
|
||||
// IP address exclusion
|
||||
// private & local networks
|
||||
'(?!(?:10|127)(?:\\.\\d{1,3}){3})' +
|
||||
'(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' +
|
||||
'(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
|
||||
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broacast addresses
|
||||
// (first & last IP address of each class)
|
||||
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
|
||||
'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
|
||||
'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
|
||||
'|' +
|
||||
|
||||
// host name
|
||||
'(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
|
||||
|
||||
// domain name
|
||||
'(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' +
|
||||
|
||||
// TLD identifier
|
||||
'(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' +
|
||||
|
||||
// TLD may end with dot
|
||||
'\\.?' +
|
||||
')' +
|
||||
|
||||
// port number
|
||||
'(?::\\d{2,5})?' +
|
||||
|
||||
// resource path
|
||||
'(?:[/?#]\\S*)?', 'img'
|
||||
);
|
@ -18,7 +18,8 @@
|
||||
"react-intl": "^2.1.2",
|
||||
"react-modal": "^1.2.1",
|
||||
"react-router": "^2.4.0",
|
||||
"underscore": "~1.8.3"
|
||||
"underscore": "~1.8.3",
|
||||
"react-autosize-textarea": "~0.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.3.6",
|
||||
|
Loading…
Reference in New Issue
Block a user