Merge pull request #7969 from KDSBrowne/002-move-chat-props

Move props for typing indicator from chat to message-form
This commit is contained in:
Anton Georgiev 2019-08-23 11:57:22 -04:00 committed by GitHub
commit 4ea7600c45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 208 additions and 95 deletions

View File

@ -4,7 +4,7 @@ import Users from '/imports/api/users';
import { UsersTyping } from '/imports/api/group-chat-msg';
import stopTyping from './stopTyping';
const TYPING_TIMEOUT = 3000;
const TYPING_TIMEOUT = 5000;
export default function startTyping(meetingId, userId, chatId) {
check(meetingId, String);
@ -25,6 +25,16 @@ export default function startTyping(meetingId, userId, chatId) {
time: (new Date()),
};
const typingUser = UsersTyping.findOne(selector, {
fields: {
time: 1,
},
});
if (typingUser) {
if (mod.time - typingUser.time <= TYPING_TIMEOUT - 100) return;
}
const cb = (err) => {
if (err) {
return Logger.error(`Typing indicator update error: ${err}`);

View File

@ -21,18 +21,6 @@ const intlMessages = defineMessages({
id: 'app.chat.hideChatLabel',
description: 'aria-label for hiding chat button',
},
singularTyping: {
id: 'app.chat.singularTyping',
description: 'used to indicate when 1 user is typing',
},
pluralTyping: {
id: 'app.chat.pluralTyping',
description: 'used to indicate when multiple user are typing',
},
severalPeople: {
id: 'app.chat.severalPeople',
description: 'displayed when 4 or more users are typing',
},
});
const Chat = (props) => {
const {
@ -46,10 +34,6 @@ const Chat = (props) => {
intl,
shortcuts,
isMeteorConnected,
typingUsers,
currentUserId,
startUserTyping,
stopUserTyping,
lastReadMessageTime,
hasUnreadMessages,
scrollPosition,
@ -61,47 +45,6 @@ const Chat = (props) => {
const HIDE_CHAT_AK = shortcuts.hidePrivateChat;
const CLOSE_CHAT_AK = shortcuts.closePrivateChat;
let names = [];
names = typingUsers.map((user) => {
const currentChatPartner = chatID;
const { userId: typingUserId, isTypingTo, name } = user;
let userNameTyping = null;
userNameTyping = currentUserId !== typingUserId ? name : userNameTyping;
const isPrivateMsg = currentChatPartner !== isTypingTo;
if (isPrivateMsg) {
const isMsgParticipant = typingUserId === currentChatPartner && currentUserId === isTypingTo;
userNameTyping = isMsgParticipant ? name : null;
}
return userNameTyping;
}).filter(e => e);
const renderIsTypingString = () => {
if (names) {
const { length } = names;
const noTypers = length < 1;
const singleTyper = length === 1;
const multipleTypersShown = length > 1 && length <= 3;
if (noTypers) return null;
if (singleTyper) {
if (names[0].length < 20) {
return ` ${names[0]} ${intl.formatMessage(intlMessages.singularTyping)}`;
}
return (` ${names[0].slice(0, 20)}... ${intl.formatMessage(intlMessages.singularTyping)}`);
}
if (multipleTypersShown) {
const formattedNames = names.map((name) => {
if (name.length < 15) return ` ${name}`;
return ` ${name.slice(0, 15)}...`;
});
return (`${formattedNames} ${intl.formatMessage(intlMessages.pluralTyping)}`);
}
return (` ${intl.formatMessage(intlMessages.severalPeople)} ${intl.formatMessage(intlMessages.pluralTyping)}`);
}
};
return (
<div
data-test="publicChat"
@ -165,9 +108,6 @@ const Chat = (props) => {
chatName,
minMessageLength,
maxMessageLength,
renderIsTypingString,
startUserTyping,
stopUserTyping,
}}
chatId={chatID}
chatTitle={title}

View File

@ -3,9 +3,6 @@ import { defineMessages, injectIntl } from 'react-intl';
import { withTracker } from 'meteor/react-meteor-data';
import { Session } from 'meteor/session';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import { UsersTyping } from '/imports/api/group-chat-msg';
import { makeCall } from '/imports/ui/services/api';
import Chat from './component';
import ChatService from './service';
@ -41,9 +38,10 @@ class ChatContainer extends PureComponent {
}
render() {
const { children } = this.props;
return (
<Chat {...this.props}>
{this.props.children}
{children}
</Chat>
);
}
@ -148,28 +146,7 @@ export default injectIntl(withTracker(({ intl }) => {
const { connected: isMeteorConnected } = Meteor.status();
const typingUsers = UsersTyping.find({
meetingId: Auth.meetingID,
$or: [
{ isTypingTo: PUBLIC_CHAT_KEY },
{ isTypingTo: Auth.userID },
],
}).fetch();
const currentUser = Users.findOne({
meetingId: Auth.meetingID,
userId: Auth.userID,
}, {
fields: {
userId: 1,
},
});
return {
startUserTyping: chatId => makeCall('startUserTyping', chatId),
stopUserTyping: () => makeCall('stopUserTyping'),
currentUserId: currentUser ? currentUser.userId : null,
typingUsers,
chatID,
chatName,
title,

View File

@ -4,6 +4,7 @@ import cx from 'classnames';
import TextareaAutosize from 'react-autosize-textarea';
import browser from 'browser-detect';
import PropTypes from 'prop-types';
import TypingIndicatorContainer from './typing-indicator/container';
import { styles } from './styles.scss';
import Button from '../../button/component';
@ -22,7 +23,6 @@ const propTypes = {
connected: PropTypes.bool.isRequired,
locked: PropTypes.bool.isRequired,
partnerIsLoggedOut: PropTypes.bool.isRequired,
renderIsTypingString: PropTypes.func.isRequired,
stopUserTyping: PropTypes.func.isRequired,
startUserTyping: PropTypes.func.isRequired,
};
@ -56,6 +56,18 @@ const messages = defineMessages({
errorChatLocked: {
id: 'app.chat.locked',
},
singularTyping: {
id: 'app.chat.singularTyping',
description: 'used to indicate when 1 user is typing',
},
pluralTyping: {
id: 'app.chat.pluralTyping',
description: 'used to indicate when multiple user are typing',
},
severalPeople: {
id: 'app.chat.severalPeople',
description: 'displayed when 4 or more users are typing',
},
});
const CHAT_ENABLED = Meteor.settings.public.chat.enabled;
@ -202,6 +214,7 @@ class MessageForm extends PureComponent {
}
const handleUserTyping = () => {
if (error) return;
startUserTyping(chatId);
};
@ -265,7 +278,6 @@ class MessageForm extends PureComponent {
disabled,
className,
chatAreaId,
renderIsTypingString,
} = this.props;
const { hasErrors, error, message } = this.state;
@ -307,12 +319,7 @@ class MessageForm extends PureComponent {
onClick={() => {}}
/>
</div>
<div className={error ? styles.error : styles.info}>
<span>
<span>{error || renderIsTypingString()}</span>
{!error && renderIsTypingString() ? <span className={styles.connectingAnimation} /> : null}
</span>
</div>
<TypingIndicatorContainer {...{ error }} />
</form>
) : null;
}

View File

@ -1,9 +1,11 @@
import React, { PureComponent } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { makeCall } from '/imports/ui/services/api';
import ChatForm from './component';
import ChatService from '../service';
const CHAT_CONFIG = Meteor.settings.public.chat;
class ChatContainer extends PureComponent {
render() {
return (
@ -17,7 +19,14 @@ export default withTracker(() => {
ChatService.updateScrollPosition(null);
return ChatService.sendGroupMessage(message);
};
const startUserTyping = chatId => makeCall('startUserTyping', chatId);
const stopUserTyping = () => makeCall('stopUserTyping');
return {
startUserTyping,
stopUserTyping,
UnsentMessagesCollection: ChatService.UnsentMessagesCollection,
minMessageLength: CHAT_CONFIG.min_message_length,
maxMessageLength: CHAT_CONFIG.max_message_length,

View File

@ -0,0 +1,111 @@
import React, { PureComponent } from 'react';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import browser from 'browser-detect';
import PropTypes from 'prop-types';
import { styles } from '../styles.scss';
const propTypes = {
intl: intlShape.isRequired,
currentChatPartner: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
typingUsers: PropTypes.arrayOf(Object).isRequired,
};
const messages = defineMessages({
singularTyping: {
id: 'app.chat.singularTyping',
description: 'used to indicate when 1 user is typing',
},
pluralTyping: {
id: 'app.chat.pluralTyping',
description: 'used to indicate when multiple user are typing',
},
severalPeople: {
id: 'app.chat.severalPeople',
description: 'displayed when 4 or more users are typing',
},
});
class TypingIndicator extends PureComponent {
constructor(props) {
super(props);
this.BROWSER_RESULTS = browser();
this.renderIsTypingString = this.renderIsTypingString.bind(this);
}
renderIsTypingString() {
const {
intl, typingUsers, currentUserId, currentChatPartner, indicatorEnabled,
} = this.props;
if (!indicatorEnabled) return null;
let names = [];
names = typingUsers.map((user) => {
const { userId: typingUserId, isTypingTo, name } = user;
let userNameTyping = null;
userNameTyping = currentUserId !== typingUserId ? name : userNameTyping;
const isPrivateMsg = currentChatPartner !== isTypingTo;
if (isPrivateMsg) {
const isMsgParticipant = typingUserId === currentChatPartner
&& currentUserId === isTypingTo;
userNameTyping = isMsgParticipant ? name : null;
}
return userNameTyping;
}).filter(e => e);
if (names) {
const { length } = names;
const noTypers = length < 1;
const singleTyper = length === 1;
const multipleTypersShown = length > 1 && length <= 3;
if (noTypers) return null;
if (singleTyper) {
if (names[0].length < 20) {
return ` ${names[0]} ${intl.formatMessage(messages.singularTyping)}`;
}
return (` ${names[0].slice(0, 20)}... ${intl.formatMessage(messages.singularTyping)}`);
}
if (multipleTypersShown) {
const formattedNames = names.map((name) => {
if (name.length < 15) return ` ${name}`;
return ` ${name.slice(0, 15)}...`;
});
return (`${formattedNames} ${intl.formatMessage(messages.pluralTyping)}`);
}
return (` ${intl.formatMessage(messages.severalPeople)} ${intl.formatMessage(messages.pluralTyping)}`);
}
return null;
}
render() {
const {
error,
} = this.props;
return (
<div className={error ? styles.error : styles.info}>
<span>
<span>{error || this.renderIsTypingString()}</span>
{!error && this.renderIsTypingString()
? <span className={styles.connectingAnimation} />
: null
}
</span>
</div>
);
}
}
TypingIndicator.propTypes = propTypes;
export default injectIntl(TypingIndicator);

View File

@ -0,0 +1,53 @@
import React, { PureComponent } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Auth from '/imports/ui/services/auth';
import { UsersTyping } from '/imports/api/group-chat-msg';
import Users from '/imports/api/users';
import TypingIndicator from './component';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled;
class TypingIndicatorContainer extends PureComponent {
render() {
return (
<TypingIndicator {...this.props} />
);
}
}
export default withTracker(() => {
const idChatOpen = Session.get('idChatOpen');
let selector = {
meetingId: Auth.meetingID,
isTypingTo: PUBLIC_CHAT_KEY,
};
if (idChatOpen !== PUBLIC_CHAT_KEY) {
selector = {
meetingId: Auth.meetingID,
isTypingTo: Auth.userID,
userId: idChatOpen,
};
}
const typingUsers = UsersTyping.find(selector).fetch();
const currentUser = Users.findOne({
meetingId: Auth.meetingID,
userId: Auth.userID,
}, {
fields: {
userId: 1,
},
});
return {
currentUserId: currentUser ? currentUser.userId : null,
typingUsers,
currentChatPartner: idChatOpen,
indicatorEnabled: TYPING_INDICATOR_ENABLED,
};
})(TypingIndicatorContainer);

View File

@ -9,10 +9,12 @@ import AnnotationsTextService from '/imports/ui/components/whiteboard/annotation
import AnnotationsLocal from '/imports/ui/components/whiteboard/service';
import mapUser from '/imports/ui/services/user/mapUser';
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_ENABLED = CHAT_CONFIG.enabled;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public;
const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled;
const SUBSCRIPTIONS = [
'users', 'meetings', 'polls', 'presentations', 'slides', 'slide-positions', 'captions',
'voiceUsers', 'whiteboard-multi-user', 'screenshare', 'group-chat',
@ -54,7 +56,9 @@ export default withTracker(() => {
};
let subscriptionsHandlers = SUBSCRIPTIONS.map((name) => {
if (!CHAT_ENABLED && name.indexOf('chat') !== -1) return;
if ((!TYPING_INDICATOR_ENABLED && name.indexOf('typing') !== -1)
|| (!CHAT_ENABLED && name.indexOf('chat') !== -1)) return;
return Meteor.subscribe(
name,
credentials,

View File

@ -171,6 +171,8 @@ public:
storage_key: UNREAD_CHATS
system_messages_keys:
chat_clear: PUBLIC_CHAT_CLEAR
typingIndicator:
enabled: true
note:
enabled: false
url: ETHERPAD_HOST