diff --git a/.eslintrc.json b/.eslintrc.json
index ced7e7206..7f0e42bf7 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -27,7 +27,8 @@
"expect": true,
"before": true,
"beforeEach": true,
- "after": true
+ "after": true,
+ "afterEach": true
},
"rules": {
"array-bracket-spacing": [2, "never"],
diff --git a/app/actions/views/root.js b/app/actions/views/root.js
index 7e308488b..c80376465 100644
--- a/app/actions/views/root.js
+++ b/app/actions/views/root.js
@@ -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([
{
diff --git a/app/scenes/select_team/select_team.js b/app/scenes/select_team/select_team.js
index 442d9f5f6..d44a2d567 100644
--- a/app/scenes/select_team/select_team.js
+++ b/app/scenes/select_team/select_team.js
@@ -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(
-
- );
-
- // }
+ if (this.props.myMembers.hasOwnProperty(id)) {
+ teams.push(
+
+ );
+ }
}
return (
diff --git a/package.json b/package.json
index 78775797e..c591b4374 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/service/actions/channels.js b/service/actions/channels.js
index 77777e35b..d85c3eaf1 100644
--- a/service/actions/channels.js
+++ b/service/actions/channels.js
@@ -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,
diff --git a/service/actions/posts.js b/service/actions/posts.js
index 9cd6d76cb..00993afc3 100644
--- a/service/actions/posts.js
+++ b/service/actions/posts.js
@@ -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);
diff --git a/service/actions/users.js b/service/actions/users.js
index 1a12c0b46..ac232d22d 100644
--- a/service/actions/users.js
+++ b/service/actions/users.js
@@ -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
}
diff --git a/service/actions/websocket.js b/service/actions/websocket.js
new file mode 100644
index 000000000..7efa1bc69
--- /dev/null
+++ b/service/actions/websocket.js
@@ -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);
+ }
+}
diff --git a/service/client/client.js b/service/client/client.js
index dac34aa67..3ac3b93ff 100644
--- a/service/client/client.js
+++ b/service/client/client.js
@@ -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);
diff --git a/service/client/websocket_client.js b/service/client/websocket_client.js
new file mode 100644
index 000000000..aacd682f8
--- /dev/null
+++ b/service/client/websocket_client.js
@@ -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();
diff --git a/service/constants/constants.js b/service/constants/constants.js
index 8c5ef28fd..14038f9fc 100644
--- a/service/constants/constants.js
+++ b/service/constants/constants.js
@@ -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;
diff --git a/service/constants/general.js b/service/constants/general.js
index e60d1cec1..5200c997f 100644
--- a/service/constants/general.js
+++ b/service/constants/general.js
@@ -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;
diff --git a/service/constants/index.js b/service/constants/index.js
index 5fe222e42..7385ca577 100644
--- a/service/constants/index.js
+++ b/service/constants/index.js
@@ -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
};
diff --git a/service/constants/websocket.js b/service/constants/websocket.js
new file mode 100644
index 000000000..e0887e0ce
--- /dev/null
+++ b/service/constants/websocket.js
@@ -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;
diff --git a/service/reducers/entities/channels.js b/service/reducers/entities/channels.js
index ece62fc2c..4188b2589 100644
--- a/service/reducers/entities/channels.js
+++ b/service/reducers/entities/channels.js
@@ -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:
diff --git a/service/reducers/entities/posts.js b/service/reducers/entities/posts.js
index f0f565790..4beeafcc9 100644
--- a/service/reducers/entities/posts.js
+++ b/service/reducers/entities/posts.js
@@ -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 = {
diff --git a/service/reducers/entities/teams.js b/service/reducers/entities/teams.js
index b3acd7179..41e2bd491 100644
--- a/service/reducers/entities/teams.js
+++ b/service/reducers/entities/teams.js
@@ -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:
diff --git a/service/reducers/requests/general.js b/service/reducers/requests/general.js
index 9008af3b7..6d8867377 100644
--- a/service/reducers/requests/general.js
+++ b/service/reducers/requests/general.js
@@ -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
});
diff --git a/test/service/actions/channels.test.js b/test/service/actions/channels.test.js
index 4fe1512d2..24c7a92d4 100644
--- a/test/service/actions/channels.test.js
+++ b/test/service/actions/channels.test.js
@@ -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 () => {
diff --git a/test/service/actions/posts.test.js b/test/service/actions/posts.test.js
index a345758a2..bcb8ef80e 100644
--- a/test/service/actions/posts.test.js
+++ b/test/service/actions/posts.test.js
@@ -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();
+ }
}
});
diff --git a/test/service/actions/teams.test.js b/test/service/actions/teams.test.js
index b923b477e..489e25bd1 100644
--- a/test/service/actions/teams.test.js
+++ b/test/service/actions/teams.test.js
@@ -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 () => {
diff --git a/test/service/actions/users.test.js b/test/service/actions/users.test.js
index 21b2f0bcf..5aa5685c1 100644
--- a/test/service/actions/users.test.js
+++ b/test/service/actions/users.test.js
@@ -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);
});
});
diff --git a/test/service/actions/websocket.test.js b/test/service/actions/websocket.test.js
new file mode 100644
index 000000000..ca925be7b
--- /dev/null
+++ b/test/service/actions/websocket.test.js
@@ -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();
+ });
+});