Merge pull request #4124 from Klauswk/html5-2x-chat-options
[HTML5 2.0] Add public chat new options
This commit is contained in:
commit
6afb7ca8b3
@ -1,7 +1,9 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis2x';
|
||||
import handleChatMessage from './handlers/chatMessage';
|
||||
import handleChatHistory from './handlers/chatHistory';
|
||||
import handleChatPublicHistoryClear from './handlers/chatPublicHistoryClear';
|
||||
|
||||
RedisPubSub.on('GetChatHistoryRespMsg', handleChatHistory);
|
||||
RedisPubSub.on('SendPublicMessageEvtMsg', handleChatMessage);
|
||||
RedisPubSub.on('SendPrivateMessageEvtMsg', handleChatMessage);
|
||||
RedisPubSub.on('ClearPublicChatHistoryEvtMsg', handleChatPublicHistoryClear);
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Chat from '/imports/api/2.0/chat';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import addChat from './../modifiers/addChat';
|
||||
|
||||
export default function publicHistoryClear({ header }, meetingId) {
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_USERID = CHAT_CONFIG.public_userid;
|
||||
const PUBLIC_CHAT_USERNAME = CHAT_CONFIG.public_username;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
|
||||
if (meetingId) {
|
||||
Chat.remove({ meetingId, 'message.toUserId': PUBLIC_CHAT_USERID },
|
||||
Logger.info(`Cleared Chats (${meetingId})`));
|
||||
|
||||
addChat(meetingId, {
|
||||
message: '<b><i>The public chat history was cleared by a moderator</i></b>',
|
||||
fromTime: new Date().getTime(),
|
||||
toUserId: PUBLIC_CHAT_USERID,
|
||||
toUsername: PUBLIC_CHAT_USERNAME,
|
||||
fromUserId: SYSTEM_CHAT_TYPE,
|
||||
fromUsername: '',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import mapToAcl from '/imports/startup/mapToAcl';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import sendChat from './methods/sendChat';
|
||||
import clearPublicChatHistory from './methods/clearPublicChatHistory';
|
||||
|
||||
Meteor.methods(mapToAcl(['methods.sendChat'], {
|
||||
Meteor.methods(mapToAcl(['methods.sendChat', 'methods.clearPublicChatHistory'], {
|
||||
sendChat,
|
||||
clearPublicChatHistory,
|
||||
}));
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis2x';
|
||||
|
||||
export default function clearPublicChatHistory(credentials) {
|
||||
const REDIS_CONFIG = Meteor.settings.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const { meetingId, requesterUserId, requesterToken } = credentials;
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(requesterToken, String);
|
||||
|
||||
const eventName = 'ClearPublicChatHistoryPubMsg';
|
||||
|
||||
const header = {
|
||||
meetingId,
|
||||
name: eventName,
|
||||
userId: requesterUserId,
|
||||
};
|
||||
|
||||
const body = {};
|
||||
|
||||
return RedisPubSub.publish(CHANNEL, eventName, meetingId, body, header);
|
||||
}
|
143
bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx
Executable file
143
bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx
Executable file
@ -0,0 +1,143 @@
|
||||
import React, { Component } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import cx from 'classnames';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import Clipboard from 'clipboard';
|
||||
import _ from 'lodash';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import Dropdown from '/imports/ui/components/dropdown/component';
|
||||
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 DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Acl from '/imports/startup/acl';
|
||||
import ChatService from './../service';
|
||||
import styles from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
clear: {
|
||||
id: 'app.chat.dropdown.clear',
|
||||
description: 'Clear button label',
|
||||
},
|
||||
save: {
|
||||
id: 'app.chat.dropdown.save',
|
||||
description: 'Clear button label',
|
||||
},
|
||||
copy: {
|
||||
id: 'app.chat.dropdown.copy',
|
||||
description: 'Copy button label',
|
||||
},
|
||||
options: {
|
||||
id: 'app.chat.dropdown.options',
|
||||
description: 'Chat Options',
|
||||
},
|
||||
});
|
||||
|
||||
class ChatDropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isSettingOpen: false,
|
||||
};
|
||||
|
||||
this.onActionsShow = this.onActionsShow.bind(this);
|
||||
this.onActionsHide = this.onActionsHide.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.clipboard = new Clipboard('#clipboardButton', {
|
||||
text: () => ChatService.exportChat(ChatService.getPublicMessages()),
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clipboard.destroy();
|
||||
}
|
||||
|
||||
onActionsShow() {
|
||||
this.setState({
|
||||
isSettingOpen: true,
|
||||
});
|
||||
}
|
||||
|
||||
onActionsHide() {
|
||||
this.setState({
|
||||
isSettingOpen: false,
|
||||
});
|
||||
}
|
||||
|
||||
getAvailableActions() {
|
||||
const { intl } = this.props;
|
||||
|
||||
const clearIcon = 'delete';
|
||||
const saveIcon = 'save_notes';
|
||||
const copyIcon = 'copy';
|
||||
|
||||
return _.compact([
|
||||
(<DropdownListItem
|
||||
icon={saveIcon}
|
||||
label={intl.formatMessage(intlMessages.save)}
|
||||
key={_.uniqueId('action-item-')}
|
||||
onClick={() => {
|
||||
const link = document.createElement('a');
|
||||
const mimeType = 'text/plain';
|
||||
|
||||
link.setAttribute('download', 'chat.txt');
|
||||
link.setAttribute('href', `data: ${mimeType} ;charset=utf-8,
|
||||
${encodeURIComponent(ChatService.exportChat(ChatService.getPublicMessages()))}`);
|
||||
link.click();
|
||||
}}
|
||||
/>),
|
||||
(<DropdownListItem
|
||||
icon={copyIcon}
|
||||
id="clipboardButton"
|
||||
label={intl.formatMessage(intlMessages.copy)}
|
||||
key={_.uniqueId('action-item-')}
|
||||
/>),
|
||||
(Acl.can('methods.clearPublicChatHistory', Auth.credentials) ?
|
||||
<DropdownListItem
|
||||
icon={clearIcon}
|
||||
label={intl.formatMessage(intlMessages.clear)}
|
||||
key={_.uniqueId('action-item-')}
|
||||
onClick={ChatService.clearPublicChatHistory}
|
||||
/>
|
||||
: null),
|
||||
]);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
|
||||
const availableActions = this.getAvailableActions();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
isOpen={this.state.isSettingOpen}
|
||||
onShow={this.onActionsShow}
|
||||
onHide={this.onActionsHide}
|
||||
>
|
||||
<DropdownTrigger tabIndex={0}>
|
||||
<Button
|
||||
label={intl.formatMessage(intlMessages.options)}
|
||||
icon="more"
|
||||
circle
|
||||
hideLabel
|
||||
className={cx(styles.btn, styles.btnSettings)}
|
||||
// FIXME: Without onClick react proptypes keep warning
|
||||
// even after the DropdownTrigger inject an onClick handler
|
||||
onClick={() => null}
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownContent placement="bottom right">
|
||||
<DropdownList>
|
||||
{availableActions}
|
||||
</DropdownList>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withModalMounter(injectIntl(ChatDropdown));
|
19
bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/styles.scss
Executable file
19
bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/styles.scss
Executable file
@ -0,0 +1,19 @@
|
||||
@import "/imports/ui/stylesheets/variables/_all";
|
||||
@import "/imports/ui/stylesheets/mixins/_scrollable";
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
|
||||
i{
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
span {
|
||||
background-color: transparent !important;
|
||||
color: $color-white !important;
|
||||
opacity: .75;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import styles from './styles';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import styles from './styles';
|
||||
import MessageForm from './message-form/component';
|
||||
import MessageList from './message-list/component';
|
||||
import ChatDropdownContainer from './chat-dropdown/component';
|
||||
import Icon from '../icon/component';
|
||||
|
||||
const ELEMENT_ID = 'chat-messages';
|
||||
@ -19,12 +20,7 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
class Chat extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Chat = (props) => {
|
||||
const {
|
||||
chatID,
|
||||
chatName,
|
||||
@ -39,7 +35,7 @@ class Chat extends Component {
|
||||
maxMessageLength,
|
||||
actions,
|
||||
intl,
|
||||
} = this.props;
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.chat}>
|
||||
@ -56,7 +52,7 @@ class Chat extends Component {
|
||||
</div>
|
||||
<div className={styles.closeIcon}>
|
||||
{
|
||||
((this.props.chatID == 'public') ?
|
||||
((chatID === 'public') ?
|
||||
null :
|
||||
<Link
|
||||
to="/users"
|
||||
@ -65,6 +61,11 @@ class Chat extends Component {
|
||||
>
|
||||
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)} />
|
||||
</Link>)
|
||||
}{
|
||||
((chatID === 'public') ?
|
||||
<ChatDropdownContainer />
|
||||
:
|
||||
null)
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
@ -91,7 +92,6 @@ class Chat extends Component {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(Chat);
|
||||
|
@ -39,7 +39,7 @@ export default injectIntl(createContainer(({ params, intl }) => {
|
||||
let chatName = title;
|
||||
|
||||
if (chatID === PUBLIC_CHAT_KEY) {
|
||||
messages = ChatService.getPublicMessages();
|
||||
messages = ChatService.reducedPublicMessages((ChatService.getPublicMessages()));
|
||||
} else {
|
||||
messages = ChatService.getPrivateMessages(chatID);
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
import Chats from '/imports/api/2.0/chat';
|
||||
import Users from '/imports/api/2.0/users';
|
||||
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import UnreadMessages from '/imports/ui/services/unread-messages';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import mapUser from '/imports/ui/services/user/mapUser';
|
||||
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import _ from 'lodash';
|
||||
|
||||
@ -82,17 +80,8 @@ const reduceMessages = (previous, current) => {
|
||||
return previous.concat(reducedMessages);
|
||||
};
|
||||
|
||||
const getPublicMessages = () => {
|
||||
const publicMessages = Chats.find({
|
||||
'message.toUsername': { $in: [PUBLIC_CHAT_USERNAME, SYSTEM_CHAT_TYPE] },
|
||||
}, {
|
||||
sort: ['message.fromTime'],
|
||||
}).fetch();
|
||||
|
||||
return publicMessages
|
||||
.reduce(reduceMessages, [])
|
||||
.map(mapMessage);
|
||||
};
|
||||
const reducedPublicMessages = publicMessages =>
|
||||
(publicMessages.reduce(reduceMessages, []).map(mapMessage));
|
||||
|
||||
const getPrivateMessages = (userID) => {
|
||||
const messages = Chats.find({
|
||||
@ -191,6 +180,8 @@ const updateUnreadMessage = (receiverID, timestamp) => {
|
||||
return UnreadMessages.update(chatType, timestamp);
|
||||
};
|
||||
|
||||
const clearPublicChatHistory = () => (makeCall('clearPublicChatHistory'));
|
||||
|
||||
const closePrivateChat = (chatID) => {
|
||||
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
|
||||
|
||||
@ -201,7 +192,41 @@ const closePrivateChat = (chatID) => {
|
||||
}
|
||||
};
|
||||
|
||||
// We decode to prevent HTML5 escaped characters.
|
||||
const htmlDecode = (input) => {
|
||||
const e = document.createElement('div');
|
||||
e.innerHTML = input;
|
||||
return e.childNodes[0].nodeValue;
|
||||
};
|
||||
|
||||
const formatTime = time => (time <= 9 ? `0${time}` : time);
|
||||
|
||||
// Export the chat as [Hour:Min] user : message
|
||||
const exportChat = messageList => (
|
||||
messageList.map(({ message }) => {
|
||||
const date = new Date(message.fromTime);
|
||||
const hour = formatTime(date.getHours());
|
||||
const min = formatTime(date.getMinutes());
|
||||
const hourMin = `${hour}:${min}`;
|
||||
if (message.fromUserId === SYSTEM_CHAT_TYPE) {
|
||||
return `[${hourMin}] ${message.message}`;
|
||||
}
|
||||
return `[${hourMin}] ${message.fromUsername}: ${htmlDecode(message.message)}`;
|
||||
}).join('\n')
|
||||
);
|
||||
|
||||
const getPublicMessages = () => {
|
||||
const publicMessages = Chats.find({
|
||||
'message.toUsername': { $in: [PUBLIC_CHAT_USERNAME, SYSTEM_CHAT_TYPE] },
|
||||
}, {
|
||||
sort: ['message.fromTime'],
|
||||
}).fetch();
|
||||
|
||||
return publicMessages;
|
||||
};
|
||||
|
||||
export default {
|
||||
reducedPublicMessages,
|
||||
getPublicMessages,
|
||||
getPrivateMessages,
|
||||
getUser,
|
||||
@ -213,4 +238,6 @@ export default {
|
||||
updateUnreadMessage,
|
||||
sendMessage,
|
||||
closePrivateChat,
|
||||
exportChat,
|
||||
clearPublicChatHistory,
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from '../styles';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import styles from '../styles';
|
||||
|
||||
const propTypes = {
|
||||
icon: PropTypes.string,
|
||||
@ -12,6 +12,9 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
icon: '',
|
||||
label: '',
|
||||
description: '',
|
||||
};
|
||||
|
||||
export default class DropdownListItem extends Component {
|
||||
@ -22,7 +25,6 @@ export default class DropdownListItem extends Component {
|
||||
}
|
||||
|
||||
renderDefault() {
|
||||
const children = [];
|
||||
const { icon, label } = this.props;
|
||||
|
||||
return [
|
||||
@ -32,11 +34,12 @@ export default class DropdownListItem extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, description, children, injectRef, tabIndex, onClick, onKeyDown,
|
||||
className, style, separator, intl, } = this.props;
|
||||
const { id, label, description, children, injectRef, tabIndex, onClick, onKeyDown,
|
||||
className, style } = this.props;
|
||||
|
||||
return (
|
||||
<li
|
||||
id={id}
|
||||
ref={injectRef}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
|
@ -19,6 +19,7 @@
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.23.0",
|
||||
"classnames": "^2.2.5",
|
||||
"clipboard": "^1.7.1",
|
||||
"eventemitter2": "^2.1.3",
|
||||
"flat": "^2.0.1",
|
||||
"hiredis": "^0.5.0",
|
||||
|
@ -25,6 +25,7 @@ acl:
|
||||
- 'kickUser'
|
||||
- 'muteUser'
|
||||
- 'unmuteUser'
|
||||
- 'clearPublicChatHistory'
|
||||
presenter:
|
||||
methods:
|
||||
- 'assignPresenter'
|
||||
|
@ -16,6 +16,10 @@
|
||||
"app.chat.closeChatLabel": "Close {0}",
|
||||
"app.chat.hideChatLabel": "Hide {0}",
|
||||
"app.chat.moreMessages": "More messages below",
|
||||
"app.chat.dropdown.options": "Chat Options",
|
||||
"app.chat.dropdown.clear": "Clear",
|
||||
"app.chat.dropdown.copy": "Copy",
|
||||
"app.chat.dropdown.save": "Save",
|
||||
"app.userlist.menuTitleContext": "available options",
|
||||
"app.userlist.chatlistitem.unreadSingular": "{0} New Message",
|
||||
"app.userlist.chatlistitem.unreadPlural": "{0} New Messages",
|
||||
|
Loading…
Reference in New Issue
Block a user