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:
commit
4ea7600c45
@ -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}`);
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
@ -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);
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user