Commit 8b9ea8ed authored by Jesús Espino's avatar Jesús Espino Committed by GitHub

Removing user typing store (#910)

* Removing user typing store

* Using makeGetUsersTyping... selector instead of getUsersTyping...

* Adding test for the component

* Using selectors instead of accessing direcly to the state
parent eb363a99
......@@ -15,8 +15,9 @@ import {
import {getPostThread} from 'mattermost-redux/actions/posts';
import {removeUserFromTeam} from 'mattermost-redux/actions/teams';
import {Client4} from 'mattermost-redux/client';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentChannelStats} from 'mattermost-redux/selectors/entities/channels';
import {browserHistory} from 'utils/browser_history';
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
......@@ -424,23 +425,24 @@ export function loadDefaultLocale() {
}
let lastTimeTypingSent = 0;
export function emitLocalUserTypingEvent(channelId, parentId) {
const t = Date.now();
const membersInChannel = ChannelStore.getStats(channelId).member_count;
const license = getLicense(getState());
const config = getConfig(getState());
if (license.IsLicensed === 'true' && config.ExperimentalTownSquareIsReadOnly === 'true') {
const channel = ChannelStore.getChannelById(channelId);
if (channel && ChannelStore.isDefault(channel)) {
return;
export function emitLocalUserTypingEvent(channelId, parentPostId) {
const userTyping = async (actionDispatch, actionGetState) => {
const state = actionGetState();
const config = getConfig(state);
const t = Date.now();
const stats = getCurrentChannelStats(state);
const membersInChannel = stats ? stats.member_count : 0;
if (((t - lastTimeTypingSent) > config.TimeBetweenUserTypingUpdatesMilliseconds) &&
(membersInChannel < config.MaxNotificationsPerChannel) && (config.EnableUserTypingMessages === 'true')) {
WebSocketClient.userTyping(channelId, parentPostId);
lastTimeTypingSent = t;
}
}
if (((t - lastTimeTypingSent) > config.TimeBetweenUserTypingUpdatesMilliseconds) && membersInChannel < config.MaxNotificationsPerChannel && config.EnableUserTypingMessages === 'true') {
WebSocketClient.userTyping(channelId, parentId);
lastTimeTypingSent = t;
}
return {data: true};
};
return dispatch(userTyping);
}
export function emitRemoteUserTypingEvent(channelId, userId, postParentId) {
......
......@@ -4,13 +4,14 @@
import $ from 'jquery';
import {batchActions} from 'redux-batched-actions';
import {ChannelTypes, EmojiTypes, PostTypes, TeamTypes, UserTypes} from 'mattermost-redux/action_types';
import {WebsocketEvents, General} from 'mattermost-redux/constants';
import {getChannelAndMyMember, getChannelStats, viewChannel} from 'mattermost-redux/actions/channels';
import {setServerVersion} from 'mattermost-redux/actions/general';
import {getPosts, getProfilesAndStatusesForPosts, getCustomEmojiForReaction} from 'mattermost-redux/actions/posts';
import * as TeamActions from 'mattermost-redux/actions/teams';
import {getMe} from 'mattermost-redux/actions/users';
import {getMe, getStatusesByIds, getProfilesByIds} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {getCurrentUser, getCurrentUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
import {getMyTeams} from 'mattermost-redux/selectors/entities/teams';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
......@@ -524,10 +525,37 @@ function handlePreferencesDeletedEvent(msg) {
}
function handleUserTypingEvent(msg) {
GlobalActions.emitRemoteUserTypingEvent(msg.broadcast.channel_id, msg.data.user_id, msg.data.parent_id);
const state = getState();
const config = getConfig(state);
const currentUserId = getCurrentUserId(state);
const currentUser = getCurrentUser(state);
const userId = msg.data.user_id;
const data = {
id: msg.broadcast.channel_id + msg.data.parent_id,
userId,
now: Date.now(),
};
if (msg.data.user_id !== UserStore.getCurrentId()) {
UserStore.setStatus(msg.data.user_id, UserStatuses.ONLINE);
dispatch({
type: WebsocketEvents.TYPING,
data,
}, getState);
setTimeout(() => {
dispatch({
type: WebsocketEvents.STOP_TYPING,
data,
}, getState);
}, parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds, 10));
if (!currentUser && userId !== currentUserId) {
getProfilesByIds([userId])(dispatch, getState);
}
const status = getStatusForUserId(state, userId);
if (status !== General.ONLINE) {
getStatusesByIds([userId])(dispatch, getState);
}
}
......
......@@ -11,7 +11,7 @@ import ConfirmModal from 'components/confirm_modal.jsx';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
import FilePreview from 'components/file_preview.jsx';
import FileUpload from 'components/file_upload';
import MsgTyping from 'components/msg_typing.jsx';
import MsgTyping from 'components/msg_typing';
import PostDeletedModal from 'components/post_deleted_modal.jsx';
import EmojiIcon from 'components/svg/emoji_icon';
import Textbox from 'components/textbox.jsx';
......@@ -646,7 +646,7 @@ export default class CreateComment extends React.PureComponent {
</div>
<MsgTyping
channelId={this.props.channelId}
parentId={this.props.rootId}
postId={this.props.rootId}
/>
<div className='post-create-footer'>
<input
......
......@@ -19,7 +19,7 @@ import ConfirmModal from 'components/confirm_modal.jsx';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
import FilePreview from 'components/file_preview.jsx';
import FileUpload from 'components/file_upload';
import MsgTyping from 'components/msg_typing.jsx';
import MsgTyping from 'components/msg_typing';
import PostDeletedModal from 'components/post_deleted_modal.jsx';
import EmojiIcon from 'components/svg/emoji_icon';
import Textbox from 'components/textbox.jsx';
......@@ -892,7 +892,7 @@ export default class CreatePost extends React.Component {
>
<MsgTyping
channelId={currentChannel.id}
parentId=''
postId=''
/>
{postError}
{preview}
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import UserTypingStore from 'stores/user_typing_store.jsx';
class MsgTyping extends React.Component {
constructor(props) {
super(props);
this.onTypingChange = this.onTypingChange.bind(this);
this.updateTypingText = this.updateTypingText.bind(this);
this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this);
this.state = {
text: '',
};
}
componentWillMount() {
UserTypingStore.addChangeListener(this.onTypingChange);
this.onTypingChange();
}
componentWillUnmount() {
UserTypingStore.removeChangeListener(this.onTypingChange);
}
componentWillReceiveProps(nextProps) {
if (this.props.channelId !== nextProps.channelId) {
this.updateTypingText(UserTypingStore.getUsersTyping(nextProps.channelId, nextProps.parentId));
}
}
onTypingChange() {
this.updateTypingText(UserTypingStore.getUsersTyping(this.props.channelId, this.props.parentId));
}
updateTypingText(typingUsers) {
let text = '';
let users = {};
let numUsers = 0;
if (typingUsers) {
users = Object.keys(typingUsers);
numUsers = users.length;
}
switch (numUsers) {
case 0:
text = '';
break;
case 1:
text = (
<FormattedMessage
id='msg_typing.isTyping'
defaultMessage='{user} is typing...'
values={{
user: users[0],
}}
/>
);
break;
default: {
const last = users.pop();
text = (
<FormattedMessage
id='msg_typing.areTyping'
defaultMessage='{users} and {last} are typing...'
values={{
users: (users.join(', ')),
last,
}}
/>
);
break;
}
}
this.setState({text});
}
render() {
return (
<span className='msg-typing'>{this.state.text}</span>
);
}
}
MsgTyping.propTypes = {
channelId: PropTypes.string,
parentId: PropTypes.string,
};
export default MsgTyping;
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {makeGetUsersTypingByChannelAndPost} from 'mattermost-redux/selectors/entities/typing';
import MsgTyping from './msg_typing.jsx';
function makeMapStateToProps() {
const getUsersTypingByChannelAndPost = makeGetUsersTypingByChannelAndPost();
return function mapStateToProps(state, ownProps) {
const typingUsers = getUsersTypingByChannelAndPost(state, {channelId: ownProps.channelId, postId: ownProps.postId});
return {
typingUsers,
};
};
}
export default connect(makeMapStateToProps)(MsgTyping);
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
export default class MsgTyping extends React.Component {
static propTypes = {
typingUsers: PropTypes.array.isRequired,
channelId: PropTypes.string.isRequired,
postId: PropTypes.string,
}
getTypingText = () => {
let users = [];
let numUsers = 0;
if (this.props.typingUsers) {
users = [...this.props.typingUsers];
numUsers = users.length;
}
if (numUsers === 0) {
return '';
}
if (numUsers === 1) {
return (
<FormattedMessage
id='msg_typing.isTyping'
defaultMessage='{user} is typing...'
values={{
user: users[0],
}}
/>
);
}
const last = users.pop();
return (
<FormattedMessage
id='msg_typing.areTyping'
defaultMessage='{users} and {last} are typing...'
values={{
users: (users.join(', ')),
last,
}}
/>
);
}
render() {
return (
<span className='msg-typing'>{this.getTypingText()}</span>
);
}
}
......@@ -4,6 +4,7 @@
import EventEmitter from 'events';
import * as Selectors from 'mattermost-redux/selectors/entities/posts';
import {PostTypes} from 'mattermost-redux/action_types';
import BrowserStore from 'stores/browser_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
......@@ -202,6 +203,7 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
dispatch({...action, type: ActionTypes.EDIT_POST});
break;
case ActionTypes.RECEIVED_POST_SELECTED:
dispatch({data: action.postId, type: PostTypes.RECEIVED_POST_SELECTED});
dispatch({...action, type: ActionTypes.SELECT_POST});
break;
case ActionTypes.RECEIVED_POST_PINNED:
......
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import EventEmitter from 'events';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import UserStore from 'stores/user_store.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import store from 'stores/redux_store.jsx';
const ActionTypes = Constants.ActionTypes;
const CHANGE_EVENT = 'change';
class UserTypingStoreClass extends EventEmitter {
constructor() {
super();
// All typeing users by channel
// this.typingUsers.[channelId+postParentId].user if present then user us typing
// Value is timeout to remove user
this.typingUsers = {};
}
emitChange() {
this.emit(CHANGE_EVENT);
}
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
}
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
nameFromId(userId) {
let name = Utils.localizeMessage('msg_typing.someone', 'Someone');
if (UserStore.hasProfile(userId)) {
name = Utils.displayUsername(userId);
}
return name;
}
userTyping(channelId, userId, postParentId) {
const name = this.nameFromId(userId);
// Key representing a location where users can type
const loc = channelId + postParentId;
// Create entry
if (!this.typingUsers[loc]) {
this.typingUsers[loc] = {};
}
// If we already have this user, clear it's timeout to be deleted
if (this.typingUsers[loc][name]) {
clearTimeout(this.typingUsers[loc][name].timeout);
}
// Set the user and a timeout to remove it
const config = getConfig(store.getState());
this.typingUsers[loc][name] = setTimeout(() => {
Reflect.deleteProperty(this.typingUsers[loc], name);
if (this.typingUsers[loc] === {}) {
Reflect.deleteProperty(this.typingUsers, loc);
}
this.emitChange();
}, parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds, 10));
this.emitChange();
}
getUsersTyping(channelId, postParentId) {
// Key representing a location where users can type
const loc = channelId + postParentId;
return this.typingUsers[loc];
}
userPosted(userId, channelId, postParentId) {
const name = this.nameFromId(userId);
const loc = channelId + postParentId;
if (this.typingUsers[loc]) {
clearTimeout(this.typingUsers[loc][name]);
Reflect.deleteProperty(this.typingUsers[loc], name);
if (this.typingUsers[loc] === {}) {
Reflect.deleteProperty(this.typingUsers, loc);
}
this.emitChange();
}
}
}
var UserTypingStore = new UserTypingStoreClass();
UserTypingStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
case ActionTypes.RECEIVED_POST:
UserTypingStore.userPosted(action.post.user_id, action.post.channel_id, action.post.parent_id);
break;
case ActionTypes.USER_TYPING:
UserTypingStore.userTyping(action.channelId, action.userId, action.postParentId);
break;
}
});
export default UserTypingStore;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/MsgTyping should match snapshot, on multiple users typing 1`] = `
<span
className="msg-typing"
>
<FormattedMessage
defaultMessage="{users} and {last} are typing..."
id="msg_typing.areTyping"
values={
Object {
"last": "another.user",
"users": "test.user, other.test.user",
}
}
/>
</span>
`;
exports[`components/MsgTyping should match snapshot, on nobody typing 1`] = `
<span
className="msg-typing"
/>
`;
exports[`components/MsgTyping should match snapshot, on one user typing 1`] = `
<span
className="msg-typing"
>
<FormattedMessage
defaultMessage="{user} is typing..."
id="msg_typing.isTyping"
values={
Object {
"user": "test.user",
}
}
/>
</span>
`;
......@@ -37,9 +37,9 @@ exports[`components/CreateComment should match snapshot read only channel 1`] =
/>
</div>
</div>
<MsgTyping
<Connect(MsgTyping)
channelId="g6139tbospd18cmxroesdk3kkc"
parentId=""
postId=""
/>
<div
className="post-create-footer"
......@@ -157,9 +157,9 @@ exports[`components/CreateComment should match snapshot, comment with message 1`
</span>
</div>
</div>
<MsgTyping
<Connect(MsgTyping)
channelId="g6139tbospd18cmxroesdk3kkc"
parentId=""
postId=""
/>
<div
className="post-create-footer"
......@@ -258,9 +258,9 @@ exports[`components/CreateComment should match snapshot, emoji picker disabled 1
</span>
</div>
</div>
<MsgTyping
<Connect(MsgTyping)
channelId="g6139tbospd18cmxroesdk3kkc"
parentId=""
postId=""
/>
<div
className="post-create-footer"
......@@ -389,9 +389,9 @@ exports[`components/CreateComment should match snapshot, empty comment 1`] = `
</span>
</div>
</div>
<MsgTyping
<Connect(MsgTyping)
channelId="g6139tbospd18cmxroesdk3kkc"
parentId=""
postId=""
/>
<div
className="post-create-footer"
......@@ -509,9 +509,9 @@ exports[`components/CreateComment should match snapshot, non-empty message and u
</span>
</div>
</div>
<MsgTyping
<Connect(MsgTyping)
channelId="g6139tbospd18cmxroesdk3kkc"
parentId=""
postId=""
/>
<div
className="post-create-footer"
......
......@@ -79,9 +79,9 @@ exports[`components/create_post should match snapshot for center textbox 1`] = `
className="post-create-footer"
id="postCreateFooter"
>
<MsgTyping
<Connect(MsgTyping)
channelId="owsyt8n43jfxjpzh9np93mx1wa"
parentId=""
postId=""
/>
</div>
</div>
......@@ -175,9 +175,9 @@ exports[`components/create_post should match snapshot for read only channel 1`]
className="post-create-footer"
id="postCreateFooter"
>
<MsgTyping
<Connect(MsgTyping)
channelId="owsyt8n43jfxjpzh9np93mx1wa"
parentId=""
postId=""
/>
</div>
</div>
......@@ -299,9 +299,9 @@ exports[`components/create_post should match snapshot when file upload disabled
className="post-create-footer"
id="postCreateFooter"
>
<MsgTyping
<Connect(MsgTyping)
channelId="owsyt8n43jfxjpzh9np93mx1wa"
parentId=""
postId=""
/>
</div>
</div>
......@@ -423,9 +423,9 @@ exports[`components/create_post should match snapshot, init 1`] = `
className="post-create-footer"
id="postCreateFooter"
>
<MsgTyping
<Connect(MsgTyping)
channelId="owsyt8n43jfxjpzh9np93mx1wa"
parentId=""
postId=""
/>
</div>
</div>
......
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import MsgTyping from 'components/msg_typing/msg_typing.jsx';
describe('components/MsgTyping', () => {
const baseProps = {
typingUsers: [],
channelId: 'test',
postId: null,
};
test('should match snapshot, on nobody typing', () => {
const wrapper = shallow(<MsgTyping {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on one user typing', () => {
const typingUsers = ['test.user'];
const props = {...baseProps, typingUsers};
const wrapper = shallow(<MsgTyping {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on multiple users typing', () => {
const typingUsers = ['test.user', 'other.test.user', 'another.user'];
const props = {...baseProps, typingUsers};
const wrapper = shallow(<MsgTyping {...props}/>);
expect(wrapper).toMatchSnapshot();
});
});
......@@ -200,8 +200,6 @@ export const ActionTypes = keyMirror({
SHOW_SEARCH: null,
USER_TYPING: null,
TOGGLE_ACCOUNT_SETTINGS_MODAL: null,
TOGGLE_SHORTCUTS_MODAL: null,
TOGGLE_IMPORT_THEME_MODAL: null,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment