Merge pull request #4124 from Klauswk/html5-2x-chat-options

[HTML5 2.0] Add public chat new options
This commit is contained in:
Anton Georgiev 2017-08-03 14:33:32 -04:00 committed by GitHub
commit 6afb7ca8b3
13 changed files with 349 additions and 94 deletions

View File

@ -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);

View File

@ -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;
}

View File

@ -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,
}));

View File

@ -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);
}

View 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));

View 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;
}
}
}

View File

@ -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,79 +20,78 @@ const intlMessages = defineMessages({
},
});
class Chat extends Component {
constructor(props) {
super(props);
}
const Chat = (props) => {
const {
chatID,
chatName,
title,
messages,
scrollPosition,
hasUnreadMessages,
lastReadMessageTime,
partnerIsLoggedOut,
isChatLocked,
minMessageLength,
maxMessageLength,
actions,
intl,
} = props;
render() {
const {
chatID,
chatName,
title,
messages,
scrollPosition,
hasUnreadMessages,
lastReadMessageTime,
partnerIsLoggedOut,
isChatLocked,
minMessageLength,
maxMessageLength,
actions,
intl,
} = this.props;
return (
<div className={styles.chat}>
return (
<div className={styles.chat}>
<header className={styles.header}>
<div className={styles.title}>
<Link
to="/users"
role="button"
aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })}
>
<Icon iconName="left_arrow" /> {title}
</Link>
</div>
<div className={styles.closeIcon}>
{
((chatID === 'public') ?
null :
<Link
to="/users"
role="button"
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
>
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)} />
</Link>)
}{
((chatID === 'public') ?
<ChatDropdownContainer />
:
null)
}
</div>
</header>
<header className={styles.header}>
<div className={styles.title}>
<Link
to="/users"
role="button"
aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })}
>
<Icon iconName="left_arrow" /> {title}
</Link>
</div>
<div className={styles.closeIcon}>
{
((this.props.chatID == 'public') ?
null :
<Link
to="/users"
role="button"
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
>
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)} />
</Link>)
}
</div>
</header>
<MessageList
chatId={chatID}
messages={messages}
id={ELEMENT_ID}
scrollPosition={scrollPosition}
hasUnreadMessages={hasUnreadMessages}
handleScrollUpdate={actions.handleScrollUpdate}
handleReadMessage={actions.handleReadMessage}
lastReadMessageTime={lastReadMessageTime}
partnerIsLoggedOut={partnerIsLoggedOut}
/>
<MessageForm
disabled={isChatLocked}
chatAreaId={ELEMENT_ID}
chatTitle={title}
chatName={chatName}
minMessageLength={minMessageLength}
maxMessageLength={maxMessageLength}
handleSendMessage={actions.handleSendMessage}
/>
</div>
);
}
}
<MessageList
chatId={chatID}
messages={messages}
id={ELEMENT_ID}
scrollPosition={scrollPosition}
hasUnreadMessages={hasUnreadMessages}
handleScrollUpdate={actions.handleScrollUpdate}
handleReadMessage={actions.handleReadMessage}
lastReadMessageTime={lastReadMessageTime}
partnerIsLoggedOut={partnerIsLoggedOut}
/>
<MessageForm
disabled={isChatLocked}
chatAreaId={ELEMENT_ID}
chatTitle={title}
chatName={chatName}
minMessageLength={minMessageLength}
maxMessageLength={maxMessageLength}
handleSendMessage={actions.handleSendMessage}
/>
</div>
);
};
export default injectIntl(Chat);

View File

@ -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);
}

View File

@ -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,
};

View File

@ -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}
@ -52,8 +55,8 @@ export default class DropdownListItem extends Component {
}
{
label ?
(<span id={this.labelID} key="labelledby" hidden>{label}</span>)
: null
(<span id={this.labelID} key="labelledby" hidden>{label}</span>)
: null
}
<span id={this.descID} key="describedby" hidden>{description}</span>
</li>

View File

@ -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",

View File

@ -25,6 +25,7 @@ acl:
- 'kickUser'
- 'muteUser'
- 'unmuteUser'
- 'clearPublicChatHistory'
presenter:
methods:
- 'assignPresenter'

View File

@ -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",