Squashing several bugs (#221)

* Squashing several bugs

* Fix websocket tests
This commit is contained in:
enahum 2017-02-08 10:28:27 -08:00 committed by GitHub
parent c933da341a
commit b3b3d9a39a
24 changed files with 174 additions and 203 deletions

View file

@ -19,6 +19,7 @@
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -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;
}

View file

@ -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
};

View file

@ -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) => {

View file

@ -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 (

View file

@ -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,

View file

@ -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
};
}

View file

@ -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 <ChannelMenuButton emitter={emitter}/>;
}
}
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 <Text>{'Waiting on channel'}</Text>;
}
let teamId = currentChannel.team_id;
if (currentChannel.type === Constants.DM_CHANNEL) {
teamId = currentTeam.id;
}
return (
<KeyboardAvoidingView
behavior='padding'
style={{flex: 1, backgroundColor: theme.centerChannelBg}}
keyboardVerticalOffset={65}
>
<StatusBar barStyle='light-content'/>
<ChannelPostList channel={currentChannel}/>
<PostTextbox
ref={this.attachPostTextbox}
value={this.props.postDraft}
teamId={currentChannel.team_id}
teamId={teamId}
channelId={currentChannel.id}
onChangeText={this.props.actions.handlePostDraftChanged}
/>

View file

@ -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,

View file

@ -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 (
<View style={containerStyle}>
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center'}}>
<TouchableHighlight
onPress={this.props.openLeftDrawer}
style={{height: 25, width: 25, marginLeft: 10, marginRight: 10}}
>
<Icon
name='bars'
size={25}
color={theme.sidebarHeaderTextColor}
/>
</TouchableHighlight>
</View>
<View style={{flex: 1, flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', backgroundColor: theme.sidebarHeaderBg}}>
<TouchableHighlight onPress={this.props.goToChannelInfo}>
<View>
<Text style={{color: theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold'}}>
{displayName}
</Text>
</View>
</TouchableHighlight>
</View>
<TouchableHighlight
onPress={this.props.openRightDrawer}
style={{height: 50, width: 50}}
>
<Text style={{color: theme.sidebarHeaderTextColor}}>{'>'}</Text>
</TouchableHighlight>
</View>
);
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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;
}
);

View file

@ -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 (
<View style={style.container}>
<ScrollView
style={style.scrollView}
contentContainerStyle={{backgroundColor: theme.centerChannelBg}}
>
<ChannelInfoHeader
createAt={currentChannel.create_at}
@ -156,7 +153,7 @@ class ChannelInfo extends PureComponent {
togglable={true}
/>
<View style={style.separatorContainer}>
<View style={[style.separator, {backgroundColor: this.props.theme.centerChannelBg}]}/>
<View style={style.separator}/>
</View>
<ChannelInfoRow
action={() => true}
@ -165,7 +162,7 @@ class ChannelInfo extends PureComponent {
textId='channel_header.notificationPreferences'
/>
<View style={style.separatorContainer}>
<View style={[style.separator, {backgroundColor: this.props.theme.centerChannelBg}]}/>
<View style={style.separator}/>
</View>
<ChannelInfoRow
action={this.props.actions.goToChannelMembers}
@ -175,7 +172,7 @@ class ChannelInfo extends PureComponent {
textId='channel_header.manageMembers'
/>
<View style={style.separatorContainer}>
<View style={[style.separator, {backgroundColor: this.props.theme.centerChannelBg}]}/>
<View style={style.separator}/>
</View>
<ChannelInfoRow
action={this.props.actions.goToChannelAddMembers}
@ -184,7 +181,7 @@ class ChannelInfo extends PureComponent {
textId='channel_header.addMembers'
/>
<View style={style.separatorContainer}>
<View style={[style.separator, {backgroundColor: this.props.theme.centerChannelBg}]}/>
<View style={style.separator}/>
</View>
<ChannelInfoRow
action={() => this.handleLeave()}

View file

@ -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
};
}

View file

@ -51,8 +51,7 @@ const defaults = {
},
headerStyle: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#367FB0'
alignItems: 'center'
},
allowSwipe: false
};

View file

@ -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

View file

@ -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'

View file

@ -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');
}
}
}

View file

@ -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;
}
);

View file

@ -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;
}

View file

@ -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();

View file

@ -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 () => {