Add WebSocket support (#144)

* Add WebSocket support

* fix unit tests

* Add actions to websocket and fix unit tests
This commit is contained in:
enahum 2016-12-21 14:54:23 -03:00 committed by Harrison Healey
parent e8d774b332
commit 7e426b6029
23 changed files with 1186 additions and 179 deletions

View file

@ -27,7 +27,8 @@
"expect": true,
"before": true,
"beforeEach": true,
"after": true
"after": true,
"afterEach": true
},
"rules": {
"array-bracket-spacing": [2, "never"],

View file

@ -54,14 +54,15 @@ export function setStoreFromLocalData(data) {
return;
}
// TODO: uncomment and sorround with try/catch when PLT-4167 is merged
// const teamMembers = Client.getMyTeamMembers();
const teamMembers = Object.keys(teams).map((key) => {
return {
team_id: key,
user_id: user.id
};
});
let teamMembers;
dispatch({type: TeamsTypes.MY_TEAM_MEMBERS_REQUEST}, getState);
try {
teamMembers = await Client.getMyTeamMembers();
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: TeamsTypes.MY_TEAM_MEMBERS_FAILURE, error}, getState);
return;
}
dispatch(batchActions([
{

View file

@ -41,25 +41,23 @@ export default class SelectTeam extends Component {
for (const id of Object.keys(this.props.teams)) {
const team = this.props.teams[id];
// TODO: uncomment when PLT-4167 is merged
// if (this.props.myMembers.hasOwnProperty(id)) {
teams.push(
<Button
key={id}
onPress={() => this.onSelectTeam(team)}
style={GlobalStyles.buttonListItemText}
containerStyle={GlobalStyles.buttonListItem}
>
{team.display_name}
<Icon
name='keyboard-arrow-right'
size={24}
color='#777'
/>
</Button>
);
// }
if (this.props.myMembers.hasOwnProperty(id)) {
teams.push(
<Button
key={id}
onPress={() => this.onSelectTeam(team)}
style={GlobalStyles.buttonListItemText}
containerStyle={GlobalStyles.buttonListItem}
>
{team.display_name}
<Icon
name='keyboard-arrow-right'
size={24}
color='#777'
/>
</Button>
);
}
}
return (

View file

@ -34,14 +34,15 @@
"react-test-renderer": "15.3.2",
"redux-logger": "2.7.0",
"remote-redux-devtools": "0.5.0",
"remote-redux-devtools-on-debugger": "0.6.2"
"remote-redux-devtools-on-debugger": "0.6.2",
"ws": "1.1.1"
},
"scripts": {
"check": "node_modules/.bin/eslint --ext \".js\" --ignore-pattern node_modules --quiet .",
"run-ios": "node node_modules/react-native/local-cli/cli.js run-ios",
"run-android": "node node_modules/react-native/local-cli/cli.js run-android",
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "mocha --compilers js:babel-register --require babel-polyfill",
"test": "NODE_ENV=test mocha --compilers js:babel-register --require babel-polyfill",
"postinstall": "make post-install"
}
}

View file

@ -12,6 +12,15 @@ import {forceLogoutIfNecessary} from './helpers';
import {batchActions} from 'redux-batched-actions';
import Client from 'service/client';
export function selectChannel(channelId) {
return async (dispatch, getState) => {
dispatch({
type: ChannelTypes.SELECTED_CHANNEL,
data: channelId
}, getState);
};
}
export function createChannel(channel, userId) {
return async (dispatch, getState) => {
dispatch(batchActions([
@ -171,8 +180,10 @@ export function updateChannelNotifyProps(userId, teamId, channelId, props) {
dispatch(batchActions([
{
type: ChannelTypes.RECEIVED_CHANNEL_PROPS,
data: notifyProps,
channel_id: channelId
data: {
channel_id: channelId,
notifyProps
}
},
{
type: ChannelTypes.NOTIFY_PROPS_SUCCESS
@ -247,14 +258,14 @@ export function fetchMyChannelsAndMembers(teamId) {
dispatch(batchActions([
{
type: ChannelTypes.RECEIVED_CHANNELS,
data: await channels
data: channels
},
{
type: ChannelTypes.CHANNELS_SUCCESS
},
{
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
data: await channelMembers
data: channelMembers
},
{
type: ChannelTypes.CHANNEL_MEMBERS_SUCCESS
@ -278,7 +289,7 @@ export function leaveChannel(teamId, channelId) {
dispatch(batchActions([
{
type: ChannelTypes.LEAVE_CHANNEL,
channel_id: channelId
data: channelId
},
{
type: ChannelTypes.LEAVE_CHANNEL_SUCCESS
@ -346,7 +357,7 @@ export function deleteChannel(teamId, channelId) {
dispatch(batchActions([
{
type: ChannelTypes.RECEIVED_CHANNEL_DELETED,
channel_id: channelId
data: channelId
},
{
type: ChannelTypes.DELETE_CHANNEL_SUCCESS
@ -371,8 +382,10 @@ export function updateLastViewedAt(teamId, channelId, active) {
dispatch(batchActions([
{
type: ChannelTypes.RECEIVED_LAST_VIEWED,
channel_id: channelId,
last_viewed_at: new Date().getTime()
data: {
channel_id: channelId,
last_viewed_at: new Date().getTime()
}
},
{
type: ChannelTypes.UPDATE_LAST_VIEWED_SUCCESS
@ -461,7 +474,7 @@ export function removeChannelMember(teamId, channelId, userId) {
dispatch({type: ChannelTypes.REMOVE_CHANNEL_MEMBER_REQUEST}, getState);
try {
await Client.addChannelMember(teamId, channelId, userId);
await Client.removeChannelMember(teamId, channelId, userId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.REMOVE_CHANNEL_MEMBER_FAILURE, error}, getState);
@ -482,6 +495,7 @@ export function removeChannelMember(teamId, channelId, userId) {
}
export default {
selectChannel,
createChannel,
createDirectChannel,
updateChannel,

View file

@ -5,6 +5,35 @@ import Client from 'service/client';
import {batchActions} from 'redux-batched-actions';
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
import {Constants, PostsTypes} from 'service/constants';
import {getProfilesByIds, getStatusesByIds} from './users';
async function getProfilesAndStatusesForPosts(list, dispatch, getState) {
const {profiles, statuses} = getState().entities.users;
const posts = list.posts;
const profilesToLoad = [];
const statusesToLoad = [];
Object.keys(posts).forEach((key) => {
const post = posts[key];
const userId = post.user_id;
if (!profiles[userId]) {
profilesToLoad.push(userId);
}
if (!statuses[userId]) {
statusesToLoad.push(userId);
}
});
if (profilesToLoad.length) {
await getProfilesByIds(profilesToLoad)(dispatch, getState);
}
if (statusesToLoad.length) {
await getStatusesByIds(statusesToLoad)(dispatch, getState);
}
}
export function createPost(teamId, post) {
return bindClientFunc(
@ -68,6 +97,7 @@ export function getPost(teamId, channelId, postId) {
let post;
try {
post = await Client.getPost(teamId, channelId, postId);
getProfilesAndStatusesForPosts(post, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POST_FAILURE, error}, getState);
@ -94,6 +124,7 @@ export function getPosts(teamId, channelId, offset = 0, limit = Constants.POST_C
try {
posts = await Client.getPosts(teamId, channelId, offset, limit);
getProfilesAndStatusesForPosts(posts, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POSTS_FAILURE, error}, getState);
@ -120,6 +151,7 @@ export function getPostsSince(teamId, channelId, since) {
let posts;
try {
posts = await Client.getPostsSince(teamId, channelId, since);
getProfilesAndStatusesForPosts(posts, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POSTS_SINCE_FAILURE, error}, getState);
@ -146,6 +178,7 @@ export function getPostsBefore(teamId, channelId, postId, offset = 0, limit = Co
let posts;
try {
posts = await Client.getPostsBefore(teamId, channelId, postId, offset, limit);
getProfilesAndStatusesForPosts(posts, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POSTS_BEFORE_FAILURE, error}, getState);
@ -172,6 +205,7 @@ export function getPostsAfter(teamId, channelId, postId, offset = 0, limit = Con
let posts;
try {
posts = await Client.getPostsAfter(teamId, channelId, postId, offset, limit);
getProfilesAndStatusesForPosts(posts, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POSTS_AFTER_FAILURE, error}, getState);

View file

@ -3,10 +3,7 @@
import {batchActions} from 'redux-batched-actions';
import Client from 'service/client';
// TODO: uncomment when PLT-4167 is merged
// import {Constants, PreferencesTypes, UsersTypes, TeamsTypes} from 'constants';
import {Constants, PreferencesTypes, UsersTypes} from 'service/constants';
import {Constants, PreferencesTypes, UsersTypes, TeamsTypes} from 'service/constants';
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
export function login(loginId, password, mfaToken = '') {
@ -15,16 +12,13 @@ export function login(loginId, password, mfaToken = '') {
Client.login(loginId, password, mfaToken).
then(async (data) => {
// TODO: uncomment when PLT-4167 is merged
// let teamMembers;
let teamMembers;
let preferences;
try {
// TODO: uncomment when PLT-4167 is merged
// const teamMembersRequest = Client.getMyTeamMembers();
const teamMembersRequest = Client.getMyTeamMembers();
const preferencesRequest = Client.getMyPreferences();
// TODO: uncomment when PLT-4167 is merged
// teamMembers = await teamMembersRequest;
teamMembers = await teamMembersRequest;
preferences = await preferencesRequest;
} catch (err) {
dispatch({type: UsersTypes.LOGIN_FAILURE, error: err}, getState);
@ -40,12 +34,10 @@ export function login(loginId, password, mfaToken = '') {
type: PreferencesTypes.RECEIVED_PREFERENCES,
data: await preferences
},
// TODO: uncomment when PLT-4167 is merged
// {
// type: TeamsTypes.RECEIVED_MY_TEAM_MEMBERS,
// data: await teamMembers
// },
{
type: TeamsTypes.RECEIVED_MY_TEAM_MEMBERS,
data: await teamMembers
},
{
type: UsersTypes.LOGIN_SUCCESS
}

View file

@ -0,0 +1,377 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import Client from 'service/client';
import websocketClient from 'service/client/websocket_client';
import {batchActions} from 'redux-batched-actions';
import {
Constants,
ChannelTypes,
GeneralTypes,
PostsTypes,
PreferencesTypes,
TeamsTypes,
UsersTypes,
WebsocketEvents
} from 'service/constants';
import {
fetchMyChannelsAndMembers,
getChannel,
getChannelStats,
updateLastViewedAt
} from 'service/actions/channels';
import {
getPosts,
getPostsSince
} from 'service/actions/posts';
import {getProfilesByIds, getStatusesByIds} from 'service/actions/users';
export function init(siteUrl, token) {
return async (dispatch, getState) => {
dispatch({type: GeneralTypes.WEBSOCKET_REQUEST}, getState);
const config = getState().entities.general.config;
let connUrl = siteUrl || Client.getUrl();
const authToken = token || Client.getToken();
// replace the protocol with a websocket one
if (connUrl.startsWith('https:')) {
connUrl = connUrl.replace(/^https:/, 'wss:');
} else {
connUrl = connUrl.replace(/^http:/, 'ws:');
}
// append a port number if one isn't already specified
if (!(/:\d+$/).test(connUrl)) {
if (connUrl.startsWith('wss:')) {
connUrl += ':' + (config.WebsocketSecurePort || 443);
} else {
connUrl += ':' + (config.WebsocketPort || 80);
}
}
connUrl += `${Client.getUrlVersion()}/users/websocket`;
websocketClient.setFirstConnectCallback(handleFirstConnect);
websocketClient.setEventCallback(handleEvent);
websocketClient.setReconnectCallback(handleReconnect);
websocketClient.setCloseCallback(handleClose);
return websocketClient.initialize(connUrl, authToken, dispatch, getState);
};
}
export function close() {
return async (dispatch, getState) => {
websocketClient.close();
if (dispatch) {
dispatch({type: GeneralTypes.WEBSOCKET_FAILURE, error: 'Closed'}, getState);
}
};
}
function handleFirstConnect(dispatch, getState) {
dispatch({type: GeneralTypes.WEBSOCKET_SUCCESS}, getState);
}
function handleReconnect(dispatch, getState) {
const entities = getState().entities;
const currentTeamId = entities.teams.currentId;
const currentChannelId = entities.channels.currentId;
if (currentTeamId) {
fetchMyChannelsAndMembers(currentTeamId)(dispatch, getState);
if (currentChannelId) {
loadPostsHelper(currentTeamId, currentChannelId, dispatch, getState);
}
}
dispatch({type: GeneralTypes.WEBSOCKET_SUCCESS}, getState);
}
function handleClose(connectFailCount, dispatch, getState) {
dispatch({type: GeneralTypes.WEBSOCKET_FAILURE, error: connectFailCount}, getState);
}
function handleEvent(msg, dispatch, getState) {
switch (msg.event) {
case WebsocketEvents.POSTED:
case WebsocketEvents.EPHEMERAL_MESSAGE:
handleNewPostEvent(msg, dispatch, getState);
break;
case WebsocketEvents.POST_EDITED:
handlePostEdited(msg, dispatch, getState);
break;
case WebsocketEvents.POST_DELETED:
handlePostDeleted(msg, dispatch, getState);
break;
case WebsocketEvents.LEAVE_TEAM:
handleLeaveTeamEvent(msg, dispatch, getState);
break;
case WebsocketEvents.USER_ADDED:
handleUserAddedEvent(msg, dispatch, getState);
break;
case WebsocketEvents.USER_REMOVED:
handleUserRemovedEvent(msg, dispatch, getState);
break;
case WebsocketEvents.USER_UPDATED:
handleUserUpdatedEvent(msg, dispatch, getState);
break;
case WebsocketEvents.CHANNEL_VIEWED:
handleChannelViewedEvent(msg, dispatch, getState);
break;
case WebsocketEvents.CHANNEL_DELETED:
handleChannelDeletedEvent(msg, dispatch, getState);
break;
case WebsocketEvents.DIRECT_ADDED:
handleDirectAddedEvent(msg, dispatch, getState);
break;
case WebsocketEvents.PREFERENCE_CHANGED:
handlePreferenceChangedEvent(msg, dispatch, getState);
break;
case WebsocketEvents.STATUS_CHANGED:
handleStatusChangedEvent(msg, dispatch, getState);
break;
}
}
function handleNewPostEvent(msg, dispatch, getState) {
const state = getState();
const users = state.entities.users;
const channels = state.entities.channels;
const teams = state.entities.teams;
const {posts} = state.entities.posts;
const isActive = state.entities.general.appState;
const post = JSON.parse(msg.data.post);
const userId = post.user_id;
const teamId = msg.data.team_id;
const status = users.statuses[userId];
if (!users.profiles[userId]) {
getProfilesByIds([userId])(dispatch, getState);
}
if (status !== Constants.ONLINE) {
getStatusesByIds([userId])(dispatch, getState);
}
if (post.channel_id === channels.currentId) {
if (isActive) {
updateLastViewedAt(teamId, post.channel_id)(dispatch, getState);
} else {
getChannel(teamId, post.channel_id)(dispatch, getState);
}
} else if (teamId === teams.currentId || msg.data.channel_type === Constants.DM_CHANNEL) {
getChannel(teamId, post.channel_id)(dispatch, getState);
}
if (post.root_id && !posts[post.root_id]) {
Client.getPost(teamId, post.channel_id, post.root_id).then((data) => {
const rootUserId = data.posts[post.root_id].user_id;
const rootStatus = users.statuses[rootUserId];
if (!users.profiles[rootUserId]) {
getProfilesByIds([rootUserId])(dispatch, getState);
}
if (rootStatus !== Constants.ONLINE) {
getStatusesByIds([rootUserId])(dispatch, getState);
}
dispatch({
type: PostsTypes.RECEIVED_POSTS,
data,
channelId: post.channel_id
}, getState);
});
}
dispatch({
type: PostsTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[post.id]: post
}
},
channelId: post.channel_id
}, getState);
}
function handlePostEdited(msg, dispatch, getState) {
const state = getState();
const channels = state.entities.channels;
const isActive = state.entities.general.appState;
const data = JSON.parse(msg.data.post);
dispatch({type: PostsTypes.RECEIVED_POST, data}, getState);
if (msg.broadcast.channel_id === channels.currentId && isActive) {
// FIXME: Update post should include team_id in the message cause its not always the current team
// updateLastViewedAt(msg.data.team_id, data.channel_id)(dispatch, getState);
}
}
function handlePostDeleted(msg, dispatch, getState) {
const data = JSON.parse(msg.data.post);
dispatch({type: PostsTypes.POST_DELETED, data}, getState);
}
function handleLeaveTeamEvent(msg, dispatch, getState) {
const entities = getState().entities;
const teams = entities.teams;
const users = entities.users;
if (users.currentId === msg.data.user_id) {
dispatch({type: TeamsTypes.LEAVE_TEAM, data: teams.teams[msg.data.team_id]}, getState);
// if they are on the team being removed deselect the current team and channel
if (teams.currentId === msg.data.team_id) {
dispatch(batchActions([
{
type: TeamsTypes.SELECT_TEAM,
data: ''
},
{
type: ChannelTypes.SELECTED_CHANNEL,
data: ''
}
]), getState);
}
}
}
function handleUserAddedEvent(msg, dispatch, getState) {
const state = getState();
const channels = state.entities.channels;
const teams = state.entities.teams;
const users = state.entities.users;
const teamId = msg.data.team_id;
if (msg.broadcast.channel_id === channels.currentId) {
getChannelStats(teamId, channels.currentId)(dispatch, getState);
}
if (teamId === teams.currentId && msg.data.user_id === users.currentId) {
getChannel(teamId, msg.broadcast.channel_id)(dispatch, getState);
}
}
function handleUserRemovedEvent(msg, dispatch, getState) {
const state = getState();
const channels = state.entities.channels;
const teams = state.entities.teams;
const users = state.entities.users;
const teamId = teams.currentId;
if (msg.broadcast.user_id === users.currentId && teamId) {
fetchMyChannelsAndMembers(teamId)(dispatch, getState);
} else if (msg.broadcast.channel_id === channels.currentId) {
getChannelStats(teamId, channels.currentId)(dispatch, getState);
}
}
function handleUserUpdatedEvent(msg, dispatch, getState) {
const entities = getState().entities;
const users = entities.users;
const user = msg.data.user;
if (user.id !== users.currentId) {
dispatch({
type: UsersTypes.RECEIVED_PROFILES,
data: {
[user.id]: user
}
}, getState);
}
}
function handleChannelViewedEvent(msg, dispatch, getState) {
const state = getState();
const channels = state.entities.channels;
const teams = state.entities.teams;
const users = state.entities.users;
if (teams.currentId === msg.broadcast.team_id && channels.currentId !== msg.data.channel_id &&
users.currentId === msg.broadcast.user_id) {
getChannel(teams.currentId, msg.data.channel_id)(dispatch, getState);
}
}
function handleChannelDeletedEvent(msg, dispatch, getState) {
const entities = getState().entities;
const {channels, currentId} = entities.channels;
const teams = entities.teams;
if (msg.broadcast.team_id === teams.currentId) {
dispatch({type: ChannelTypes.RECEIVED_CHANNEL_DELETED, data: msg.data.channel_id}, getState);
if (msg.data.channel_id === currentId) {
let channelId = '';
const channel = Object.keys(channels).filter((key) => channels[key].name === Constants.DEFAULT_CHANNEL);
if (channel.length) {
channelId = channel[0];
}
dispatch({type: ChannelTypes.SELECTED_CHANNEL, data: channelId}, getState);
}
fetchMyChannelsAndMembers(teams.currentId)(dispatch, getState);
}
}
function handleDirectAddedEvent(msg, dispatch, getState) {
const state = getState();
const teams = state.entities.teams;
getChannel(teams.currentId, msg.broadcast.channel_id)(dispatch, getState);
}
function handlePreferenceChangedEvent(msg, dispatch, getState) {
const preference = JSON.parse(msg.data.preference);
dispatch({type: PreferencesTypes.RECEIVED_PREFERENCES, data: [preference]}, getState);
if (preference.category === Constants.CATEGORY_DIRECT_CHANNEL_SHOW) {
const state = getState();
const users = state.entities.users;
const userId = preference.name;
const status = users.statuses[userId];
if (!users.profiles[userId]) {
getProfilesByIds([userId])(dispatch, getState);
}
if (status !== Constants.ONLINE) {
getStatusesByIds([userId])(dispatch, getState);
}
}
}
function handleStatusChangedEvent(msg, dispatch, getState) {
dispatch({
type: UsersTypes.RECEIVED_STATUSES,
data: {
[msg.data.user_id]: msg.data.status
}
}, getState);
}
// Helpers
function loadPostsHelper(teamId, channelId, dispatch, getState) {
const {posts, postsByChannel} = getState().entities.posts;
const postsArray = postsByChannel[channelId];
const latestPostId = postsArray[postsArray.length - 1];
let latestPostTime = 0;
if (latestPostId) {
latestPostTime = posts[latestPostId].create_at || 0;
}
if (Object.keys(posts).length === 0 || postsArray.length < Constants.POST_CHUNK_SIZE || latestPostTime === 0) {
getPosts(teamId, channelId)(dispatch, getState);
} else {
getPostsSince(teamId, channelId, latestPostTime)(dispatch, getState);
}
}

View file

@ -38,6 +38,10 @@ export default class Client {
this.token = token;
}
getUrlVersion() {
return this.urlVersion;
}
getBaseRoute() {
return `${this.url}${this.urlVersion}`;
}
@ -581,9 +585,7 @@ export default class Client {
);
};
// Client helpers
// Preferences routes
getMyPreferences = async () => {
return this.doFetch(
`${this.getPreferencesRoute()}/`,
@ -619,6 +621,7 @@ export default class Client {
);
};
// Client helpers
doFetch = async (url, options) => {
const {data} = await this.doFetchWithResponse(url, options);

View file

@ -0,0 +1,200 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
const MAX_WEBSOCKET_FAILS = 7;
const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
/* eslint-disable global-require, no-process-env */
let Socket;
if (process.env.NODE_ENV === 'test') {
Socket = require('ws');
} else {
Socket = WebSocket;
}
/* eslint-enable global-require, no-process-env */
class WebSocketClient {
constructor() {
this.conn = null;
this.connectionUrl = null;
this.sequence = 1;
this.connectFailCount = 0;
this.eventCallback = null;
this.firstConnectCallback = null;
this.reconnectCallback = null;
this.errorCallback = null;
this.closeCallback = null;
this.dispatch = null;
this.getState = null;
}
initialize(connectionUrl = this.connectionUrl, token, dispatch, getState) {
return new Promise((resolve, reject) => {
if (this.conn) {
resolve();
return;
}
if (connectionUrl == null) {
console.log('websocket must have connection url'); //eslint-disable-line no-console
reject('websocket must have connection url');
return;
}
if (!dispatch) {
console.log('websocket must have a dispatch'); //eslint-disable-line no-console
reject('websocket must have a dispatch');
return;
}
if (this.connectFailCount === 0) {
console.log('websocket connecting to ' + connectionUrl); //eslint-disable-line no-console
}
this.conn = new Socket(connectionUrl);
this.connectionUrl = connectionUrl;
this.dispatch = dispatch;
this.getState = getState;
this.conn.onopen = () => {
if (token) {
this.sendMessage('authentication_challenge', {token});
}
if (this.connectFailCount > 0) {
console.log('websocket re-established connection'); //eslint-disable-line no-console
if (this.reconnectCallback) {
this.reconnectCallback(this.dispatch, this.getState);
}
} else if (this.firstConnectCallback) {
this.firstConnectCallback(this.dispatch, this.getState);
resolve();
}
this.connectFailCount = 0;
};
this.conn.onclose = () => {
this.conn = null;
this.sequence = 1;
if (this.connectFailCount === 0) {
console.log('websocket closed'); //eslint-disable-line no-console
}
this.connectFailCount++;
if (this.closeCallback) {
this.closeCallback(this.connectFailCount, this.dispatch, this.getState);
}
let retryTime = MIN_WEBSOCKET_RETRY_TIME;
// If we've failed a bunch of connections then start backing off
if (this.connectFailCount > MAX_WEBSOCKET_FAILS) {
retryTime = MIN_WEBSOCKET_RETRY_TIME * this.connectFailCount * this.connectFailCount;
if (retryTime > MAX_WEBSOCKET_RETRY_TIME) {
retryTime = MAX_WEBSOCKET_RETRY_TIME;
}
}
setTimeout(
() => {
this.initialize(connectionUrl, token);
},
retryTime
);
};
this.conn.onerror = (evt) => {
if (this.connectFailCount <= 1) {
console.log('websocket error'); //eslint-disable-line no-console
console.log(evt); //eslint-disable-line no-console
}
if (this.errorCallback) {
this.errorCallback(evt, this.dispatch, this.getState);
}
};
this.conn.onmessage = (evt) => {
const msg = JSON.parse(evt.data);
if (msg.seq_reply) {
if (msg.error) {
console.log(msg); //eslint-disable-line no-console
}
} else if (this.eventCallback) {
this.eventCallback(msg, this.dispatch, this.getState);
}
};
});
}
setEventCallback(callback) {
this.eventCallback = callback;
}
setFirstConnectCallback(callback) {
this.firstConnectCallback = callback;
}
setReconnectCallback(callback) {
this.reconnectCallback = callback;
}
setErrorCallback(callback) {
this.errorCallback = callback;
}
setCloseCallback(callback) {
this.closeCallback = callback;
}
close() {
this.connectFailCount = 0;
this.sequence = 1;
if (this.conn && this.conn.readyState === Socket.OPEN) {
this.conn.onclose = () => {}; //eslint-disable-line no-empty-function
this.conn.close();
this.conn = null;
console.log('websocket closed'); //eslint-disable-line no-console
}
}
sendMessage(action, data) {
const msg = {
action,
seq: this.sequence++,
data
};
if (this.conn && this.conn.readyState === Socket.OPEN) {
this.conn.send(JSON.stringify(msg));
} else if (!this.conn || this.conn.readyState === Socket.CLOSED) {
this.conn = null;
this.initialize();
}
}
userTyping(channelId, parentId) {
this.sendMessage('user_typing', {
channel_id: channelId,
parent_id: parentId
});
}
getStatuses() {
this.sendMessage('get_statuses', null);
}
getStatusesByIds(userIds) {
this.sendMessage('get_statuses_by_ids', {
user_ids: userIds
});
}
}
export default new WebSocketClient();

View file

@ -6,13 +6,22 @@ const Constants = {
PROFILE_CHUNK_SIZE: 100,
CHANNELS_CHUNK_SIZE: 50,
OFFLINE: 'offline',
AWAY: 'away',
ONLINE: 'online',
TEAM_USER_ROLE: 'team_user',
TEAM_ADMIN_ROLE: 'team_admin',
CHANNEL_USER_ROLE: 'channel_user',
CHANNEL_ADMIN_ROLE: 'channel_admin',
POST_DELETED: 'DELETED'
DEFAULT_CHANNEL: 'town-square',
DM_CHANNEL: 'D',
POST_DELETED: 'DELETED',
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show'
};
export default Constants;

View file

@ -24,7 +24,11 @@ const GeneralTypes = keyMirror({
LOG_CLIENT_ERROR_REQUEST: null,
LOG_CLIENT_ERROR_SUCCESS: null,
LOG_CLIENT_ERROR_FAILURE: null
LOG_CLIENT_ERROR_FAILURE: null,
WEBSOCKET_REQUEST: null,
WEBSOCKET_SUCCESS: null,
WEBSOCKET_FAILURE: null
});
export default GeneralTypes;

View file

@ -10,6 +10,7 @@ import PostsTypes from './posts';
import PreferencesTypes from './preferences';
import RequestStatus from './request_status';
import Themes from './themes';
import WebsocketEvents from './websocket';
const Preferences = {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
@ -26,5 +27,6 @@ export {
PreferencesTypes,
Preferences,
RequestStatus,
Themes
Themes,
WebsocketEvents
};

View file

@ -0,0 +1,25 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
const WebsocketEvents = {
POSTED: 'posted',
POST_EDITED: 'post_edited',
POST_DELETED: 'post_deleted',
CHANNEL_DELETED: 'channel_deleted',
CHANNEL_VIEWED: 'channel_viewed',
DIRECT_ADDED: 'direct_added',
LEAVE_TEAM: 'leave_team',
USER_ADDED: 'user_added',
USER_REMOVED: 'user_removed',
USER_UPDATED: 'user_updated',
TYPING: 'typing',
PREFERENCE_CHANGED: 'preference_changed',
EPHEMERAL_MESSAGE: 'ephemeral_message',
STATUS_CHANGED: 'status_change',
HELLO: 'hello',
WEBRTC: 'webrtc',
REACTION_ADDED: 'reaction_added',
REACTION_REMOVED: 'reaction_removed'
};
export default WebsocketEvents;

View file

@ -6,6 +6,8 @@ import {combineReducers} from 'redux';
function currentId(state = '', action) {
switch (action.type) {
case ChannelTypes.SELECTED_CHANNEL:
return action.data;
case UsersTypes.LOGOUT_SUCCESS:
return '';
default:
@ -30,6 +32,9 @@ function channels(state = {}, action) {
}
return nextState;
}
case ChannelTypes.RECEIVED_CHANNEL_DELETED:
Reflect.deleteProperty(nextState, action.data);
return nextState;
case UsersTypes.LOGOUT_SUCCESS:
return {};
@ -56,26 +61,26 @@ function myMembers(state = {}, action) {
return nextState;
}
case ChannelTypes.RECEIVED_CHANNEL_PROPS: {
const member = {...state[action.channel_id]};
member.notify_props = action.data;
const member = {...state[action.data.channel_id]};
member.notify_props = action.data.notifyProps;
return {
...state,
[action.channel_id]: member
[action.data.channel_id]: member
};
}
case ChannelTypes.RECEIVED_LAST_VIEWED: {
const member = {...state[action.channel_id]};
member.last_viewed_at = action.last_viewed_at;
const member = {...state[action.data.channel_id]};
member.last_viewed_at = action.data.last_viewed_at;
return {
...state,
[action.channel_id]: member
[action.data.channel_id]: member
};
}
case ChannelTypes.LEAVE_CHANNEL:
case ChannelTypes.RECEIVED_CHANNEL_DELETED:
Reflect.deleteProperty(nextState, action.channel_id);
Reflect.deleteProperty(nextState, action.data);
return nextState;
case UsersTypes.LOGOUT_SUCCESS:

View file

@ -3,6 +3,13 @@
import {Constants, PostsTypes, UsersTypes} from 'service/constants';
function initialState() {
return {
posts: {},
postsByChannel: {}
};
}
function handleReceivedPost(posts = {}, postsByChannel = {}, action) {
const post = action.data;
const channelId = post.channel_id;
@ -123,7 +130,7 @@ function handleRemovePost(posts = {}, postsByChannel = {}, action) {
return {posts: nextPosts, postsByChannel: nextPostsByChannel};
}
function handlePosts(state = {}, action) {
function handlePosts(state = initialState(), action) {
switch (action.type) {
case PostsTypes.RECEIVED_POST:
return handleReceivedPost(state.posts, state.postsByChannel, action);
@ -135,10 +142,7 @@ function handlePosts(state = {}, action) {
return handleRemovePost(state.posts, state.postsByChannel, action);
case UsersTypes.LOGOUT_SUCCESS:
return {
posts: {},
postsByChannel: {}
};
return initialState();
default:
return state;
}
@ -162,7 +166,7 @@ function currentFocusedPostId(state = '', action) {
}
}
export default function(state = {}, action) {
export default function(state = initialState(), action) {
const {posts, postsByChannel} = handlePosts(state, action);
const nextState = {

View file

@ -48,8 +48,8 @@ function myMembers(state = {}, action) {
case TeamsTypes.LEAVE_TEAM: {
const nextState = {...state};
const data = action.team;
Reflect.deleteProperty(nextState, data.team_id);
const data = action.data;
Reflect.deleteProperty(nextState, data.id);
return nextState;
}
case UsersTypes.LOGOUT_SUCCESS:

View file

@ -35,8 +35,19 @@ function license(state = initialRequestState(), action) {
);
}
function websocket(state = initialRequestState(), action) {
return handleRequest(
GeneralTypes.WEBSOCKET_REQUEST,
GeneralTypes.WEBSOCKET_SUCCESS,
GeneralTypes.WEBSOCKET_FAILURE,
state,
action
);
}
export default combineReducers({
server,
config,
license
license,
websocket
});

View file

@ -4,7 +4,6 @@
import assert from 'assert';
import * as Actions from 'service/actions/channels';
import {getProfiles} from 'service/actions/users';
import Client from 'service/client';
import configureStore from 'app/store';
import {RequestStatus} from 'service/constants';
@ -66,7 +65,6 @@ describe('Actions.Channels', () => {
const state = store.getState();
const channels = state.entities.channels.channels;
const members = state.entities.channels.myMembers;
const profiles = state.entities.users.profiles;
const preferences = state.entities.preferences.myPreferences;
const createRequest = state.requests.channels.createChannel;
@ -80,7 +78,6 @@ describe('Actions.Channels', () => {
const membersCount = Object.keys(members).length;
assert.ok(channels, 'channels is empty');
assert.ok(members, 'members is empty');
assert.ok(profiles[user.id], 'profiles does not have userId');
assert.ok(Object.keys(preferences).length, 'preferences is empty');
assert.ok(channels[Object.keys(members)[0]], 'channels should have the member');
assert.ok(members[Object.keys(channels)[0]], 'members should belong to channel');
@ -93,7 +90,6 @@ describe('Actions.Channels', () => {
}
});
await getProfiles(0)(store.dispatch, store.getState);
Actions.createDirectChannel(TestHelper.basicTeam.id, TestHelper.basicUser.id, user.id)(store.dispatch, store.getState);
});
}).timeout(3000);
@ -176,7 +172,7 @@ describe('Actions.Channels', () => {
Actions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)(store.dispatch, store.getState);
});
});
}).timeout(3000);
it('updateChannelNotifyProps', (done) => {
TestHelper.initBasic(Client).then(async () => {

View file

@ -156,19 +156,23 @@ describe('Actions.Posts', () => {
channelId
)(store.dispatch, store.getState);
let shouldStop = false;
store.subscribe(() => {
const state = store.getState();
const {posts, postsByChannel} = state.entities.posts;
assert.ok(posts);
assert.ok(postsByChannel);
assert.ok(postsByChannel[channelId]);
if (!shouldStop) {
assert.ok(posts);
assert.ok(postsByChannel);
assert.ok(postsByChannel[channelId]);
assert.equal(postsByChannel[channelId].length, 0);
assert.ok(!posts[postId]);
assert.ok(!posts[post1a.id]);
assert.equal(postsByChannel[channelId].length, 0);
assert.ok(!posts[postId]);
assert.ok(!posts[post1a.id]);
done();
shouldStop = true;
done();
}
});
Actions.removePost(
@ -189,30 +193,35 @@ describe('Actions.Posts', () => {
TestHelper.fakePost(channelId)
);
let shouldStop = false;
store.subscribe(() => {
const state = store.getState();
const getRequest = state.requests.posts.getPost;
const {posts, postsByChannel} = state.entities.posts;
if (getRequest.status === RequestStatus.SUCCESS) {
assert.ok(posts);
assert.ok(postsByChannel);
assert.ok(postsByChannel[channelId]);
if (!shouldStop &&
(getRequest.status === RequestStatus.SUCCESS || getRequest.status === RequestStatus.FAILURE)) {
if (getRequest.error) {
shouldStop = true;
done(new Error(JSON.stringify(getRequest.error)));
} else {
assert.ok(posts);
assert.ok(postsByChannel);
assert.ok(postsByChannel[channelId]);
assert.ok(posts[post.id]);
assert.ok(posts[post.id]);
let found = false;
for (const postIdInChannel of postsByChannel[channelId]) {
if (postIdInChannel === post.id) {
found = true;
break;
let found = false;
for (const postIdInChannel of postsByChannel[channelId]) {
if (postIdInChannel === post.id) {
found = true;
break;
}
}
assert.ok(found, 'failed to find post in postsByChannel');
shouldStop = true;
done();
}
assert.ok(found, 'failed to find post in postsByChannel');
done();
} else if (getRequest.status === RequestStatus.FAILURE) {
done(new Error(JSON.stringify(getRequest.error)));
}
});
@ -252,30 +261,36 @@ describe('Actions.Posts', () => {
{...TestHelper.fakePost(channelId), root_id: post3.id}
);
let shouldStop = false;
store.subscribe(() => {
const state = store.getState();
const getRequest = state.requests.posts.getPosts;
const {posts, postsByChannel} = state.entities.posts;
if (getRequest.status === RequestStatus.SUCCESS) {
assert.ok(posts);
assert.ok(postsByChannel);
if (!shouldStop &&
(getRequest.status === RequestStatus.SUCCESS || getRequest.status === RequestStatus.FAILURE)) {
if (getRequest.error) {
shouldStop = true;
done(new Error(JSON.stringify(getRequest.error)));
} else {
assert.ok(posts);
assert.ok(postsByChannel);
const postsInChannel = postsByChannel[channelId];
assert.ok(postsInChannel);
assert.equal(postsInChannel[0], post3a.id, 'wrong order for post3a');
assert.equal(postsInChannel[1], post3.id, 'wrong order for post3');
assert.equal(postsInChannel[3], post1a.id, 'wrong order for post1a');
const postsInChannel = postsByChannel[channelId];
assert.ok(postsInChannel);
assert.equal(postsInChannel[0], post3a.id, 'wrong order for post3a');
assert.equal(postsInChannel[1], post3.id, 'wrong order for post3');
assert.equal(postsInChannel[3], post1a.id, 'wrong order for post1a');
assert.ok(posts[post1.id]);
assert.ok(posts[post1a.id]);
assert.ok(posts[post2.id]);
assert.ok(posts[post3.id]);
assert.ok(posts[post3a.id]);
assert.ok(posts[post1.id]);
assert.ok(posts[post1a.id]);
assert.ok(posts[post2.id]);
assert.ok(posts[post3.id]);
assert.ok(posts[post3a.id]);
done();
} else if (getRequest.status === RequestStatus.FAILURE) {
done(new Error(JSON.stringify(getRequest.error)));
shouldStop = true;
done();
}
}
});
@ -314,24 +329,30 @@ describe('Actions.Posts', () => {
{...TestHelper.fakePost(channelId), root_id: post3.id}
);
let shouldStop = false;
store.subscribe(() => {
const state = store.getState();
const getRequest = state.requests.posts.getPostsSince;
const {posts, postsByChannel} = state.entities.posts;
if (getRequest.status === RequestStatus.SUCCESS) {
assert.ok(posts);
assert.ok(postsByChannel);
if (!shouldStop &&
(getRequest.status === RequestStatus.SUCCESS || getRequest.status === RequestStatus.FAILURE)) {
if (getRequest.error) {
shouldStop = true;
done(new Error(JSON.stringify(getRequest.error)));
} else {
assert.ok(posts);
assert.ok(postsByChannel);
const postsInChannel = postsByChannel[channelId];
assert.ok(postsInChannel);
assert.equal(postsInChannel[0], post3a.id, 'wrong order for post3a');
assert.equal(postsInChannel[1], post3.id, 'wrong order for post3');
assert.equal(postsInChannel.length, 2, 'wrong size');
const postsInChannel = postsByChannel[channelId];
assert.ok(postsInChannel);
assert.equal(postsInChannel[0], post3a.id, 'wrong order for post3a');
assert.equal(postsInChannel[1], post3.id, 'wrong order for post3');
assert.equal(postsInChannel.length, 2, 'wrong size');
done();
} else if (getRequest.status === RequestStatus.FAILURE) {
done(new Error(JSON.stringify(getRequest.error)));
shouldStop = true;
done();
}
}
});
@ -371,24 +392,30 @@ describe('Actions.Posts', () => {
{...TestHelper.fakePost(channelId), root_id: post3.id}
);
let shouldStop = false;
store.subscribe(() => {
const state = store.getState();
const getRequest = state.requests.posts.getPostsBefore;
const {posts, postsByChannel} = state.entities.posts;
if (getRequest.status === RequestStatus.SUCCESS) {
assert.ok(posts);
assert.ok(postsByChannel);
if (!shouldStop &&
(getRequest.status === RequestStatus.SUCCESS || getRequest.status === RequestStatus.FAILURE)) {
if (getRequest.error) {
shouldStop = true;
done(new Error(JSON.stringify(getRequest.error)));
} else {
assert.ok(posts);
assert.ok(postsByChannel);
const postsInChannel = postsByChannel[channelId];
assert.ok(postsInChannel);
assert.equal(postsInChannel[0], post1a.id, 'wrong order for post1a');
assert.equal(postsInChannel[1], post1.id, 'wrong order for post1');
assert.equal(postsInChannel.length, 3, 'wrong size');
const postsInChannel = postsByChannel[channelId];
assert.ok(postsInChannel);
assert.equal(postsInChannel[0], post1a.id, 'wrong order for post1a');
assert.equal(postsInChannel[1], post1.id, 'wrong order for post1');
assert.equal(postsInChannel.length, 3, 'wrong size');
done();
} else if (getRequest.status === RequestStatus.FAILURE) {
done(new Error(JSON.stringify(getRequest.error)));
shouldStop = true;
done();
}
}
});
@ -430,24 +457,30 @@ describe('Actions.Posts', () => {
{...TestHelper.fakePost(channelId), root_id: post3.id}
);
let shouldStop = false;
store.subscribe(() => {
const state = store.getState();
const getRequest = state.requests.posts.getPostsAfter;
const {posts, postsByChannel} = state.entities.posts;
if (getRequest.status === RequestStatus.SUCCESS) {
assert.ok(posts);
assert.ok(postsByChannel);
if (!shouldStop &&
(getRequest.status === RequestStatus.SUCCESS || getRequest.status === RequestStatus.FAILURE)) {
if (getRequest.error) {
shouldStop = true;
done(new Error(JSON.stringify(getRequest.error)));
} else {
assert.ok(posts);
assert.ok(postsByChannel);
const postsInChannel = postsByChannel[channelId];
assert.ok(postsInChannel);
assert.equal(postsInChannel[0], post3a.id, 'wrong order for post3a');
assert.equal(postsInChannel[1], post3.id, 'wrong order for post3');
assert.equal(postsInChannel.length, 2, 'wrong size');
const postsInChannel = postsByChannel[channelId];
assert.ok(postsInChannel);
assert.equal(postsInChannel[0], post3a.id, 'wrong order for post3a');
assert.equal(postsInChannel[1], post3.id, 'wrong order for post3');
assert.equal(postsInChannel.length, 2, 'wrong size');
done();
} else if (getRequest.status === RequestStatus.FAILURE) {
done(new Error(JSON.stringify(getRequest.error)));
shouldStop = true;
done();
}
}
});

View file

@ -141,29 +141,28 @@ describe('Actions.Teams', () => {
});
});
// TODO: uncomment when PLT-4167 is merged
// it('getMyTeamMembers', (done) => {
// TestHelper.initBasic(Client).then(() => {
// const store = configureStore();
//
// store.subscribe(() => {
// const membersRequest = store.getState().requests.teams.getMyTeamMembers;
// const members = store.getState().entities.teams.myMembers;
//
// if (membersRequest.status === RequestStatus.SUCCESS || membersRequest.status === RequestStatus.FAILURE) {
// if (membersRequest.error) {
// done(new Error(JSON.stringify(membersRequest.error)));
// } else {
// assert.ok(members);
// assert.ok(members[TestHelper.basicTeam.id]);
// done();
// }
// }
// });
//
// Actions.getMyTeamMembers()(store.dispatch, store.getState);
// });
// });
it('getMyTeamMembers', (done) => {
TestHelper.initBasic(Client).then(() => {
const store = configureStore();
store.subscribe(() => {
const membersRequest = store.getState().requests.teams.getMyTeamMembers;
const members = store.getState().entities.teams.myMembers;
if (membersRequest.status === RequestStatus.SUCCESS || membersRequest.status === RequestStatus.FAILURE) {
if (membersRequest.error) {
done(new Error(JSON.stringify(membersRequest.error)));
} else {
assert.ok(members);
assert.ok(members[TestHelper.basicTeam.id]);
done();
}
}
});
Actions.getMyTeamMembers()(store.dispatch, store.getState);
});
});
it('getTeamMember', (done) => {
TestHelper.initBasic(Client).then(async () => {
@ -193,7 +192,7 @@ describe('Actions.Teams', () => {
Actions.getTeamMember(TestHelper.basicTeam.id, user.id)(store.dispatch, store.getState);
});
});
}).timeout(3000);
it('getTeamMembersByIds', (done) => {
TestHelper.initBasic(Client).then(async () => {

View file

@ -113,7 +113,7 @@ describe('Actions.Users', () => {
it('getProfiles', (done) => {
TestHelper.initBasic(Client).then(async () => {
const store = configureStore();
const user = await TestHelper.basicClient.createUser(TestHelper.fakeUser());
await TestHelper.basicClient.createUser(TestHelper.fakeUser());
store.subscribe(() => {
const profilesRequest = store.getState().requests.users.getProfiles;
@ -123,7 +123,7 @@ describe('Actions.Users', () => {
if (profilesRequest.error) {
done(new Error(JSON.stringify(profilesRequest.error)));
} else {
assert.ok(profiles[user.id]);
assert.ok(Object.keys(profiles).length);
done();
}
@ -215,7 +215,7 @@ describe('Actions.Users', () => {
}
});
Actions.getProfilesNotInChannel(TestHelper.basicTeam.id, TestHelper.basicChannel.id, 0)(store.dispatch, store.getState);
Actions.getProfilesNotInChannel(TestHelper.basicTeam.id, TestHelper.basicChannel.id, 0, 500)(store.dispatch, store.getState);
});
});

View file

@ -0,0 +1,298 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import assert from 'assert';
import * as Actions from 'service/actions/websocket';
import * as ChannelActions from 'service/actions/channels';
import * as TeamActions from 'service/actions/teams';
import * as RootActions from 'app/actions/views/root';
import Client from 'service/client';
import configureStore from 'app/store';
import {Constants, RequestStatus} from 'service/constants';
import TestHelper from 'test/test_helper';
describe('Actions.Websocket', () => {
let store;
beforeEach(async () => {
store = configureStore();
await TestHelper.initBasic(Client);
return await Actions.init()(store.dispatch, store.getState);
});
afterEach(() => {
Actions.close()();
});
it('WebSocket Connect', () => {
const ws = store.getState().requests.general.websocket;
assert.ok(ws.status === RequestStatus.SUCCESS);
});
it('Websocket Handle New Post', async () => {
const client = TestHelper.createClient();
const user = await client.createUserWithInvite(
TestHelper.fakeUser(),
null,
null,
TestHelper.basicTeam.invite_id
);
await client.login(user.email, 'password1');
await Client.addChannelMember(
TestHelper.basicTeam.id,
TestHelper.basicChannel.id,
user.id);
const post = {...TestHelper.fakePost(), channel_id: TestHelper.basicChannel.id};
await client.createPost(TestHelper.basicTeam.id, post);
const entities = store.getState().entities;
const {posts, postsByChannel} = entities.posts;
const channelId = TestHelper.basicChannel.id;
const postId = postsByChannel[channelId][0];
assert.ok(posts[postId].message.indexOf('Unit Test') > -1);
});
it('Websocket Handle Post Edited', async () => {
let post = {...TestHelper.fakePost(), channel_id: TestHelper.basicChannel.id};
const client = TestHelper.createClient();
const user = await client.createUserWithInvite(
TestHelper.fakeUser(),
null,
null,
TestHelper.basicTeam.invite_id
);
await Client.addChannelMember(
TestHelper.basicTeam.id,
TestHelper.basicChannel.id,
user.id);
await client.login(user.email, 'password1');
post = await client.createPost(TestHelper.basicTeam.id, post);
post.message += ' (edited)';
await client.editPost(TestHelper.basicTeam.id, post);
store.subscribe(async () => {
const entities = store.getState().entities;
const {posts} = entities.posts;
assert.ok(posts[post.id].message.indexOf('(edited)') > -1);
});
});
it('Websocket Handle Post Deleted', async () => {
const client = TestHelper.createClient();
const user = await client.createUserWithInvite(
TestHelper.fakeUser(),
null,
null,
TestHelper.basicTeam.invite_id
);
await Client.addChannelMember(
TestHelper.basicTeam.id,
TestHelper.basicChannel.id,
user.id);
await client.login(user.email, 'password1');
let post = TestHelper.fakePost();
post.channel_id = TestHelper.basicChannel.id;
post = await client.createPost(TestHelper.basicTeam.id, post);
await client.deletePost(TestHelper.basicTeam.id, post.channel_id, post.id);
store.subscribe(async () => {
const entities = store.getState().entities;
const {posts} = entities.posts;
assert.strictEqual(posts[post.id].state, Constants.POST_DELETED);
});
});
it('WebSocket Leave Team', async () => {
const client = TestHelper.createClient();
const user = await client.createUser(TestHelper.fakeUser());
await client.login(user.email, 'password1');
const team = await client.createTeam(TestHelper.fakeTeam());
const channel = await client.createChannel(TestHelper.fakeChannel(team.id));
await client.addUserToTeam(team.id, TestHelper.basicUser.id);
await client.addChannelMember(team.id, channel.id, TestHelper.basicUser.id);
await RootActions.setStoreFromLocalData({url: Client.getUrl(), token: Client.getToken()})(store.dispatch, store.getState);
await TeamActions.selectTeam(team)(store.dispatch, store.getState);
await ChannelActions.selectChannel(channel.id)(store.dispatch, store.getState);
await client.removeUserFromTeam(team.id, TestHelper.basicUser.id);
const {currentId} = store.getState().entities.teams;
assert.strictEqual(currentId, '');
});
it('Websocket Handle User Added', async () => {
const client = TestHelper.createClient();
const user = await client.createUserWithInvite(
TestHelper.fakeUser(),
null,
null,
TestHelper.basicTeam.invite_id
);
await TeamActions.selectTeam(TestHelper.basicTeam)(store.dispatch, store.getState);
await RootActions.setStoreFromLocalData({
url: Client.getUrl(),
token: Client.getToken()
})(store.dispatch, store.getState);
await ChannelActions.addChannelMember(
TestHelper.basicTeam.id,
TestHelper.basicChannel.id,
user.id
)(store.dispatch, store.getState);
const entities = store.getState().entities;
const profilesInChannel = entities.users.profilesInChannel;
assert.ok(profilesInChannel[TestHelper.basicChannel.id].has(user.id));
});
it('Websocket Handle User Removed', async () => {
await TeamActions.selectTeam(TestHelper.basicTeam)(store.dispatch, store.getState);
await RootActions.setStoreFromLocalData({
url: Client.getUrl(),
token: Client.getToken()
})(store.dispatch, store.getState);
await ChannelActions.removeChannelMember(
TestHelper.basicTeam.id,
TestHelper.basicChannel.id,
TestHelper.basicUser.id
)(store.dispatch, store.getState);
const state = store.getState();
const entities = state.entities;
const profilesNotInChannel = entities.users.profilesNotInChannel;
assert.ok(profilesNotInChannel[TestHelper.basicChannel.id].has(TestHelper.basicUser.id));
});
it('Websocket Handle User Updated', async () => {
const client = TestHelper.createClient();
const user = await client.createUserWithInvite(
TestHelper.fakeUser(),
null,
null,
TestHelper.basicTeam.invite_id
);
await client.login(user.email, 'password1');
await client.updateUser({...user, first_name: 'tester4'});
store.subscribe(() => {
const state = store.getState();
const entities = state.entities;
const profiles = entities.users.profiles;
assert.strictEqual(profiles[user.id].first_name, 'tester4');
});
});
it('Websocket Handle Channel Viewed', (done) => {
async function test() {
await TeamActions.selectTeam(TestHelper.basicTeam)(store.dispatch, store.getState);
await RootActions.setStoreFromLocalData({
url: Client.getUrl(),
token: Client.getToken()
})(store.dispatch, store.getState);
await Client.updateLastViewedAt(
TestHelper.basicTeam.id,
TestHelper.basicChannel.id,
false
);
setTimeout(() => {
const state = store.getState();
const entities = state.entities;
const {channels} = entities.channels;
assert.ok(channels[TestHelper.basicChannel.id]);
done();
}, 500);
}
test();
});
it('Websocket Handle Channel Deleted', (done) => {
async function test() {
await ChannelActions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)(store.dispatch, store.getState);
await TeamActions.selectTeam(TestHelper.basicTeam)(store.dispatch, store.getState);
await ChannelActions.selectChannel(TestHelper.basicChannel.id)(store.dispatch, store.getState);
await Client.deleteChannel(
TestHelper.basicTeam.id,
TestHelper.basicChannel.id
);
setTimeout(() => {
const state = store.getState();
const entities = state.entities;
const {channels, currentId} = entities.channels;
assert.ok(channels[currentId].name === Constants.DEFAULT_CHANNEL);
done();
}, 500);
}
test();
});
it('Websocket Handle Direct Channel', (done) => {
// TODO: Uncomment once fixed on platform
// const test = async () => {
// const client = TestHelper.createClient();
// const user = await client.createUserWithInvite(
// TestHelper.fakeUser(),
// null,
// null,
// TestHelper.basicTeam.invite_id
// );
//
// await client.login(user.email, 'password1');
// await TeamActions.selectTeam(TestHelper.basicTeam)(store.dispatch, store.getState);
//
// store.subscribe(() => {
// const entities = store.getState().entities;
// const {channels} = entities.channels;
// assert.ok(Object.keys(channels).length);
// done();
// });
//
// await client.createDirectChannel(TestHelper.basicTeam.id, TestHelper.basicUser.id);
// };
// test();
done();
});
it('Websocket Handle Preferences Changed', (done) => {
async function test() {
const client = TestHelper.createClient();
const user = await client.createUserWithInvite(
TestHelper.fakeUser(),
null,
null,
TestHelper.basicTeam.invite_id
);
await client.login(user.email, 'password1');
const dm = await client.createDirectChannel(TestHelper.basicTeam.id, TestHelper.basicUser.id);
const post = {...TestHelper.fakePost(), channel_id: dm.id};
await client.createPost(TestHelper.basicTeam.id, post);
setTimeout(() => {
const entities = store.getState().entities;
const preferences = entities.preferences.myPreferences;
assert.ok(preferences[`${Constants.CATEGORY_DIRECT_CHANNEL_SHOW}--${user.id}`]);
done();
}, 500);
}
test();
});
});