diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index aa625b40e..80df7330b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 41ca49db3..92493842e 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -101,8 +101,10 @@ export function selectInitialChannel(teamId) { const state = getState(); const {channels, myMembers} = state.entities.channels; const currentChannelId = state.entities.channels.currentId; + const currentChannel = channels[currentChannelId]; - if (channels[currentChannelId] && myMembers[currentChannelId] && channels[currentChannelId].team_id === teamId) { + if (currentChannel && myMembers[currentChannelId] && + (currentChannel.team_id === teamId || currentChannel.type === Constants.DM_CHANNEL)) { await selectChannel(currentChannelId)(dispatch, getState); return; } diff --git a/app/actions/views/select_team.js b/app/actions/views/select_team.js index b2ef54f21..df908fc61 100644 --- a/app/actions/views/select_team.js +++ b/app/actions/views/select_team.js @@ -23,6 +23,19 @@ export function handleTeamChange(team) { }; } +export function selectFirstAvailableTeam() { + return async (dispatch, getState) => { + const {teams: allTeams, myMembers} = getState().entities.teams; + const teams = Object.keys(myMembers).map((key) => allTeams[key]); + const firstTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0]; + + if (firstTeam) { + handleTeamChange(firstTeam)(dispatch, getState); + } + }; +} + export default { - handleTeamChange + handleTeamChange, + selectFirstAvailableTeam }; diff --git a/app/components/channel_list/channel_list.js b/app/components/channel_list/channel_list.js index 4202e2afc..04186fff6 100644 --- a/app/components/channel_list/channel_list.js +++ b/app/components/channel_list/channel_list.js @@ -5,7 +5,6 @@ import React from 'react'; import {Alert, ListView, Platform, StyleSheet, Text, View} from 'react-native'; import {injectIntl, intlShape} from 'react-intl'; -import {buildDisplayNameAndTypeComparable} from 'service/utils/channel_utils'; import {Constants} from 'service/constants'; import LineDivider from 'app/components/line_divider'; import ChannelItem from './channel_item'; @@ -79,6 +78,11 @@ class ChannelList extends React.Component { }).isRequired }; + static defaultProps = { + currentTeam: {}, + currentChannel: {} + }; + constructor(props) { super(props); this.firstUnreadChannel = null; @@ -111,39 +115,34 @@ class ChannelList extends React.Component { return data.findIndex((obj) => obj.display_name === displayName); }; - getAboveAndBelow = (index) => { - const channel = this.state.dataSource.getRowData(0, index); - const result = buildDisplayNameAndTypeComparable(channel).localeCompare(buildDisplayNameAndTypeComparable(this.props.currentChannel)); - if (result < 0) { - return {above: true, below: false}; - } else if (result > 0) { - return {above: false, below: true}; - } - return {above: false, below: false}; - }; - updateUnreadIndicators = (v) => { let showAbove = false; let showBelow = false; - if (this.firstUnreadChannel) { - const index = this.getRowIndex(this.firstUnreadChannel); - if (index >= 0 && !v.s1[index]) { - showAbove = this.getAboveAndBelow(index).above; - } - } + if (v.s1) { + const visibleIndexes = Object.keys(v.s1); + const firstVisible = parseInt(visibleIndexes[0], 10); + const lastVisible = parseInt(visibleIndexes[visibleIndexes.length - 1], 10); - if (this.lastUnreadChannel) { - const index = this.getRowIndex(this.lastUnreadChannel); - if (index >= 0 && !v.s1[index]) { - showBelow = this.getAboveAndBelow(index).below; + if (this.firstUnreadChannel) { + const index = this.getRowIndex(this.firstUnreadChannel); + if (index < firstVisible) { + showAbove = true; + } } - } - this.setState({ - showAbove, - showBelow - }); + if (this.lastUnreadChannel) { + const index = this.getRowIndex(this.lastUnreadChannel); + if (index > lastVisible) { + showBelow = true; + } + } + + this.setState({ + showAbove, + showBelow + }); + } }; onSelectChannel = (channel) => { diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 703f3b629..9342f10d6 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -19,7 +19,7 @@ const style = StyleSheet.create({ export default class PostList extends React.Component { static propTypes = { - posts: React.PropTypes.object.isRequired, + posts: React.PropTypes.array.isRequired, theme: React.PropTypes.object.isRequired }; @@ -35,7 +35,7 @@ export default class PostList extends React.Component { componentWillReceiveProps(nextProps) { this.setState({ - dataSource: this.state.dataSource.cloneWithRowsAndSections(nextProps.posts) + dataSource: this.state.dataSource.cloneWithRows(nextProps.posts) }); } @@ -45,7 +45,7 @@ export default class PostList extends React.Component { } return this.renderPost(row); - } + }; renderDateHeader = (date) => { return ( @@ -55,7 +55,7 @@ export default class PostList extends React.Component { date={date} /> ); - } + }; renderPost = (post) => { return ( diff --git a/app/components/post_textbox/post_textbox.js b/app/components/post_textbox/post_textbox.js index 5b561d38d..22b29615f 100644 --- a/app/components/post_textbox/post_textbox.js +++ b/app/components/post_textbox/post_textbox.js @@ -40,13 +40,13 @@ export default class PostTextbox extends React.PureComponent { blur = () => { this.refs.input.getWrappedInstance().blur(); - } + }; handleContentSizeChange = (e) => { this.setState({ contentHeight: e.nativeEvent.contentSize.height }); - } + }; sendMessage = () => { if (this.props.value.trim().length === 0) { @@ -63,7 +63,7 @@ export default class PostTextbox extends React.PureComponent { this.props.actions.createPost(this.props.teamId, post); this.props.onChangeText(''); - } + }; render() { const theme = this.props.theme; @@ -104,8 +104,10 @@ export default class PostTextbox extends React.PureComponent { onChangeText={this.props.onChangeText} onContentSizeChange={this.handleContentSizeChange} placeholder={placeholder} + placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)} onSubmitEditing={this.sendMessage} multiline={true} + underlineColorAndroid='transparent' style={{ borderColor: changeOpacity(theme.centerChannelColor, 0.2), borderWidth: 1, diff --git a/app/navigation/router.js b/app/navigation/router.js index e30010023..226021215 100644 --- a/app/navigation/router.js +++ b/app/navigation/router.js @@ -15,10 +15,12 @@ import Drawer from 'app/components/drawer'; import FormattedText from 'app/components/formatted_text'; import OptionsModal from 'app/components/options_modal'; import {RouteTransitions} from 'app/navigation/routes'; +import {getTheme} from 'service/selectors/entities/preferences'; class Router extends React.Component { static propTypes = { navigation: React.PropTypes.object, + theme: React.PropTypes.object, modalVisible: React.PropTypes.bool.isRequired, actions: React.PropTypes.shape({ closeDrawers: React.PropTypes.func.isRequired, @@ -72,7 +74,10 @@ class Router extends React.Component { renderLeftComponent={renderLeftComponent} renderTitleComponent={renderTitleComponent} renderRightComponent={renderRightComponent} - style={navigationProps.headerStyle} + style={[ + navigationProps.headerStyle, + {backgroundColor: this.props.theme.sidebarHeaderBg} + ]} /> ); } @@ -221,6 +226,7 @@ function mapStateToProps(state) { const modalVisible = state.views.optionsModal.visible; return { navigation: state.navigation, + theme: getTheme(state), modalVisible }; } diff --git a/app/scenes/channel/channel.js b/app/scenes/channel/channel.js index f1a59e228..ed19e3041 100644 --- a/app/scenes/channel/channel.js +++ b/app/scenes/channel/channel.js @@ -8,7 +8,9 @@ import { Text } from 'react-native'; +import {Constants} from 'service/constants'; import PostTextbox from 'app/components/post_textbox'; +import EventEmitter from 'service/utils/event_emitter'; import ChannelDrawerButton from './channel_drawer_button'; import ChannelMenuButton from './channel_menu_button'; @@ -20,6 +22,7 @@ export default class Channel extends React.PureComponent { actions: React.PropTypes.shape({ loadChannelsIfNecessary: React.PropTypes.func.isRequired, loadProfilesAndTeamMembersForDMSidebar: React.PropTypes.func.isRequired, + selectFirstAvailableTeam: React.PropTypes.func.isRequired, selectInitialChannel: React.PropTypes.func.isRequired, openChannelDrawer: React.PropTypes.func.isRequired, openRightMenuDrawer: React.PropTypes.func.isRequired, @@ -47,21 +50,13 @@ export default class Channel extends React.PureComponent { renderRightComponent: (props, emitter) => { return ; } - } - - constructor(props) { - super(props); - - this.state = { - leftSidebarOpen: false, - rightSidebarOpen: false - }; - } + }; componentWillMount() { this.props.subscribeToHeaderEvent('open_channel_drawer', this.openChannelDrawer); this.props.subscribeToHeaderEvent('open_right_menu', this.openRightMenuDrawer); this.props.subscribeToHeaderEvent('show_channel_info', this.props.actions.goToChannelInfo); + EventEmitter.on('leave_team', this.handleLeaveTeam); const teamId = this.props.currentTeam.id; this.props.actions.initWebSocket(); this.loadChannels(teamId); @@ -77,6 +72,7 @@ export default class Channel extends React.PureComponent { componentWillUnmount() { this.props.actions.closeWebSocket(); this.props.unsubscribeFromHeaderEvent('open_channel_drawer'); + EventEmitter.off('leave_team', this.handleLeaveTeam); } loadChannels = (teamId) => { @@ -96,9 +92,13 @@ export default class Channel extends React.PureComponent { this.props.actions.openRightMenuDrawer(); }; - attachPostTextbox = (c) => { - this.postTextbox = c; - } + attachPostTextbox = (ref) => { + this.postTextbox = ref; + }; + + handleLeaveTeam = () => { + this.props.actions.selectFirstAvailableTeam(); + }; render() { const { @@ -113,17 +113,23 @@ export default class Channel extends React.PureComponent { return {'Waiting on channel'}; } + let teamId = currentChannel.team_id; + if (currentChannel.type === Constants.DM_CHANNEL) { + teamId = currentTeam.id; + } + return ( diff --git a/app/scenes/channel/channel_container.js b/app/scenes/channel/channel_container.js index 487b4f6a4..8c2da991a 100644 --- a/app/scenes/channel/channel_container.js +++ b/app/scenes/channel/channel_container.js @@ -16,6 +16,7 @@ import { selectInitialChannel, handlePostDraftChanged } from 'app/actions/views/channel'; +import {selectFirstAvailableTeam} from 'app/actions/views/select_team'; import {getCurrentChannel} from 'service/selectors/entities/channels'; import {getTheme} from 'service/selectors/entities/preferences'; @@ -43,6 +44,7 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ loadChannelsIfNecessary, loadProfilesAndTeamMembersForDMSidebar, + selectFirstAvailableTeam, selectInitialChannel, openChannelDrawer, openRightMenuDrawer, diff --git a/app/scenes/channel/channel_header/channel_header.js b/app/scenes/channel/channel_header/channel_header.js deleted file mode 100644 index 25ae5d827..000000000 --- a/app/scenes/channel/channel_header/channel_header.js +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; -import { - Text, - Platform, - TouchableHighlight, - View -} from 'react-native'; -import Icon from 'react-native-vector-icons/FontAwesome'; - -export default class ChannelHeader extends React.PureComponent { - static propTypes = { - displayName: React.PropTypes.string.isRequired, - theme: React.PropTypes.object.isRequired, - openLeftDrawer: React.PropTypes.func.isRequired, - openRightDrawer: React.PropTypes.func.isRequired, - goToChannelInfo: React.PropTypes.func.isRequired - } - - render() { - const { - displayName, - theme - } = this.props; - - const containerStyle = { - backgroundColor: theme.sidebarHeaderBg, - flexDirection: 'row', - justifyContent: 'flex-start', - ...Platform.select({ - ios: { - marginTop: 20 - } - }) - }; - - return ( - - - - - - - - - - - {displayName} - - - - - - {'>'} - - - ); - } -} diff --git a/app/scenes/channel/channel_header/channel_header_container.js b/app/scenes/channel/channel_header/channel_header_container.js deleted file mode 100644 index 8ff067ddc..000000000 --- a/app/scenes/channel/channel_header/channel_header_container.js +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {connect} from 'react-redux'; - -import {Constants} from 'service/constants'; -import {getMyPreferences, getTheme} from 'service/selectors/entities/preferences'; -import {getCurrentUserId, getUser} from 'service/selectors/entities/users'; -import {getUserIdFromChannelName} from 'service/utils/channel_utils'; -import {displayUsername} from 'service/utils/user_utils'; - -import ChannelHeader from './channel_header'; - -function mapStateToProps(state, ownProps) { - const currentChannel = ownProps.currentChannel; - - let displayName = ''; - if (currentChannel) { - if (currentChannel.type === Constants.DM_CHANNEL) { - const otherUser = getUser(state, getUserIdFromChannelName(getCurrentUserId(state), currentChannel)); - - if (otherUser) { - displayName = displayUsername(otherUser, getMyPreferences(state)); - } - } else { - displayName = currentChannel.display_name; - } - } - - return { - ...ownProps, - displayName, - theme: getTheme(state) - }; -} - -export default connect(mapStateToProps)(ChannelHeader); diff --git a/app/scenes/channel/channel_header/index.js b/app/scenes/channel/channel_header/index.js deleted file mode 100644 index 337137922..000000000 --- a/app/scenes/channel/channel_header/index.js +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ChannelHeaderContainer from './channel_header_container'; - -export default ChannelHeaderContainer; diff --git a/app/scenes/channel/channel_post_list/channel_post_list.js b/app/scenes/channel/channel_post_list/channel_post_list.js index d099fa8ed..2e8303a82 100644 --- a/app/scenes/channel/channel_post_list/channel_post_list.js +++ b/app/scenes/channel/channel_post_list/channel_post_list.js @@ -11,8 +11,8 @@ export default class ChannelPostList extends React.Component { loadPostsIfNecessary: React.PropTypes.func.isRequired }).isRequired, channel: React.PropTypes.object.isRequired, - posts: React.PropTypes.object.isRequired - } + posts: React.PropTypes.array.isRequired + }; componentDidMount() { this.props.actions.loadPostsIfNecessary(this.props.channel); diff --git a/app/scenes/channel/channel_post_list/channel_post_list_container.js b/app/scenes/channel/channel_post_list/channel_post_list_container.js index b379db35a..36c360f50 100644 --- a/app/scenes/channel/channel_post_list/channel_post_list_container.js +++ b/app/scenes/channel/channel_post_list/channel_post_list_container.js @@ -62,14 +62,18 @@ const getPostsInCurrentChannelGroupedByDay = createSelector( postsByDay[dateString].push(post); } + let posts = []; + // Push the date on after the posts so that it's rendered first for (const dateString in postsByDay) { if (postsByDay.hasOwnProperty(dateString)) { postsByDay[dateString].push(new Date(dateString)); } + + posts = posts.concat(postsByDay[dateString]); } - return postsByDay; + return posts; } ); diff --git a/app/scenes/channel_info/channel_info.js b/app/scenes/channel_info/channel_info.js index 99beda72e..392270c53 100644 --- a/app/scenes/channel_info/channel_info.js +++ b/app/scenes/channel_info/channel_info.js @@ -43,7 +43,6 @@ class ChannelInfo extends PureComponent { currentChannelMemberCount: PropTypes.number, isFavorite: PropTypes.bool.isRequired, leaveChannelRequest: PropTypes.object.isRequired, - theme: PropTypes.object.isRequired, actions: PropTypes.shape({ getChannelStats: PropTypes.func.isRequired, goToChannelMembers: PropTypes.func.isRequired, @@ -53,7 +52,7 @@ class ChannelInfo extends PureComponent { goBack: PropTypes.func.isRequired, leaveChannel: PropTypes.func.isRequired }) - } + }; constructor(props) { super(props); @@ -90,7 +89,7 @@ class ChannelInfo extends PureComponent { const toggleFavorite = isFavorite ? unmarkFavorite : markFavorite; this.setState({isFavorite: !isFavorite}); toggleFavorite(currentChannel.id); - } + }; handleLeave() { const {formatMessage} = this.props.intl; @@ -122,7 +121,7 @@ class ChannelInfo extends PureComponent { renderLeaveChannelRow() { const channel = this.props.currentChannel; const isDefaultChannel = channel.name === Constants.DEFAULT_CHANNEL; - const isDirectMessage = channel.type === 'D'; + const isDirectMessage = channel.type === Constants.DM_CHANNEL; return !isDefaultChannel && !isDirectMessage; } @@ -130,15 +129,13 @@ class ChannelInfo extends PureComponent { const { currentChannel, currentChannelCreatorName, - currentChannelMemberCount, - theme + currentChannelMemberCount } = this.props; return ( - + true} @@ -165,7 +162,7 @@ class ChannelInfo extends PureComponent { textId='channel_header.notificationPreferences' /> - + - + - + this.handleLeave()} diff --git a/app/scenes/channel_info/channel_info_container.js b/app/scenes/channel_info/channel_info_container.js index e1a813852..b2eb5b9e4 100644 --- a/app/scenes/channel_info/channel_info_container.js +++ b/app/scenes/channel_info/channel_info_container.js @@ -9,7 +9,6 @@ import {goToChannelMembers, goToChannelAddMembers, goBack} from 'app/actions/nav import {getChannelStats} from 'service/actions/channels'; import {markFavorite, unmarkFavorite, leaveChannel} from 'app/actions/views/channel'; import {getCurrentChannel, getCurrentChannelStats, getChannelsByCategory} from 'service/selectors/entities/channels'; -import {getTheme} from 'service/selectors/entities/preferences'; import {getUser} from 'service/selectors/entities/users'; import ChannelInfo from './channel_info'; @@ -29,8 +28,7 @@ function mapStateToProps(state, ownProps) { currentChannelCreatorName, currentChannelMemberCount, isFavorite, - leaveChannelRequest, - theme: getTheme(state) + leaveChannelRequest }; } diff --git a/app/scenes/navigationSceneConnect.js b/app/scenes/navigationSceneConnect.js index 990e913e9..4398d264d 100644 --- a/app/scenes/navigationSceneConnect.js +++ b/app/scenes/navigationSceneConnect.js @@ -51,8 +51,7 @@ const defaults = { }, headerStyle: { flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#367FB0' + alignItems: 'center' }, allowSwipe: false }; diff --git a/app/scenes/root/root.js b/app/scenes/root/root.js index 5d08e561f..740bce95b 100644 --- a/app/scenes/root/root.js +++ b/app/scenes/root/root.js @@ -23,7 +23,7 @@ export default class Root extends React.Component { static navigationProps = { hideNavBar: true - } + }; componentDidMount() { // Any initialization logic for navigation, setting up the client, etc should go here diff --git a/fastlane/Fastfile b/fastlane/Fastfile index eacec97f0..061ba0e48 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -198,7 +198,7 @@ platform :android do desc 'Build Release file' lane :dev do - build_android({release: false}) + build_android({release: true}) end desc 'Submit a new Beta Build to Google Play' diff --git a/service/actions/websocket.js b/service/actions/websocket.js index c676ec29f..a1e032efb 100644 --- a/service/actions/websocket.js +++ b/service/actions/websocket.js @@ -2,8 +2,8 @@ // See License.txt for license information. import Client from 'service/client'; +import EventEmitter from 'service/utils/event_emitter'; import websocketClient from 'service/client/websocket_client'; -import {batchActions} from 'redux-batched-actions'; import { Constants, ChannelTypes, @@ -241,16 +241,7 @@ function handleLeaveTeamEvent(msg, dispatch, 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.SELECT_CHANNEL, - data: '' - } - ]), getState); + EventEmitter.emit('leave_team'); } } } diff --git a/service/selectors/entities/channels.js b/service/selectors/entities/channels.js index 89f40c9c7..0fcaae9b2 100644 --- a/service/selectors/entities/channels.js +++ b/service/selectors/entities/channels.js @@ -3,7 +3,7 @@ import {createSelector} from 'reselect'; import {getCurrentTeamId} from 'service/selectors/entities/teams'; -import {buildDisplayableChannelList} from 'service/utils/channel_utils'; +import {buildDisplayableChannelList, completeDirectChannelInfo} from 'service/utils/channel_utils'; function getAllChannels(state) { return state.entities.channels.channels; @@ -20,8 +20,14 @@ export function getCurrentChannelId(state) { export const getCurrentChannel = createSelector( getAllChannels, getCurrentChannelId, - (allChannels, currentChannelId) => { - return allChannels[currentChannelId]; + (state) => state.entities.users, + (state) => state.entities.preferences.myPreferences, + (allChannels, currentChannelId, users, myPreferences) => { + const channel = allChannels[currentChannelId]; + if (channel) { + return completeDirectChannelInfo(users, myPreferences, channel); + } + return channel; } ); diff --git a/service/utils/channel_utils.js b/service/utils/channel_utils.js index 0378a7733..9a57a7751 100644 --- a/service/utils/channel_utils.js +++ b/service/utils/channel_utils.js @@ -143,7 +143,7 @@ function createFakeChannelCurried(userId) { return (otherUserId) => createFakeChannel(userId, otherUserId); } -function completeDirectChannelInfo(usersState, myPreferences, channel) { +export function completeDirectChannelInfo(usersState, myPreferences, channel) { if (!isDirectChannel(channel)) { return channel; } diff --git a/service/utils/event_emitter.js b/service/utils/event_emitter.js new file mode 100644 index 000000000..93d69d02a --- /dev/null +++ b/service/utils/event_emitter.js @@ -0,0 +1,59 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +function isFunction(obj) { + return typeof obj === 'function'; +} + +class EventEmitter { + constructor() { + this.listeners = new Map(); + } + + addListener(label, callback) { + if (!this.listeners.has(label)) { + this.listeners.set(label, []); + } + this.listeners.get(label).push(callback); + } + + on(label, callback) { + this.addListener(label, callback); + } + + removeListener(label, callback) { + const listeners = this.listeners.get(label); + let index; + + if (listeners && listeners.length) { + index = listeners.reduce((i, listener, idx) => { + return (isFunction(listener) && listener === callback) ? idx : i; + }, -1); + + if (index > -1) { + listeners.splice(index, 1); + this.listeners.set(label, listeners); + return true; + } + } + return false; + } + + off(label, callback) { + this.removeListener(label, callback); + } + + emit(label, ...args) { + const listeners = this.listeners.get(label); + + if (listeners && listeners.length) { + listeners.forEach((listener) => { + listener(...args); + }); + return true; + } + return false; + } +} + +export default new EventEmitter(); diff --git a/test/service/actions/websocket.test.js b/test/service/actions/websocket.test.js index 9c7979114..b2cf07261 100644 --- a/test/service/actions/websocket.test.js +++ b/test/service/actions/websocket.test.js @@ -134,8 +134,8 @@ describe('Actions.Websocket', () => { 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, ''); + const {myMembers} = store.getState().entities.teams; + assert.ifError(myMembers[team.id]); }); it('Websocket Handle User Added', async () => {