RN-345 Additional improvements to channel switching (#926)

* RN-345 Only show refreshing indicator when channel is actually refreshing

* Added getCurrentLocale selector

* Added SwitchTeams component to channel drawer to reduce rerenders

* Reduced number of props passed from ChannelsList into children

* Removed unused prop from FilteredList

* Moved visible post calculation out of render function

* Don't set the channel to loading when not switching channels

* Added most of channel view state to blacklist
This commit is contained in:
Harrison Healey 2017-09-22 14:48:30 -04:00 committed by Jarred Witt
parent 05bfc04ec9
commit 98abbd6dca
20 changed files with 380 additions and 173 deletions

View file

@ -325,6 +325,13 @@ export function setChannelLoading(loading = true) {
};
}
export function setChannelRefreshing(loading = true) {
return {
type: ViewTypes.SET_CHANNEL_REFRESHING,
loading
};
}
export function setPostTooltipVisible(visible = true) {
return {
type: ViewTypes.POST_TOOLTIP_VISIBLE,

View file

@ -206,9 +206,12 @@ export default class ChannelDrawer extends PureComponent {
} = actions;
markChannelAsRead(channel.id, currentChannelId);
setChannelLoading();
viewChannel(currentChannelId);
setChannelDisplayName(channel.display_name);
if (channel.id !== currentChannelId) {
setChannelLoading();
viewChannel(currentChannelId);
setChannelDisplayName(channel.display_name);
}
this.closeChannelDrawer();

View file

@ -5,7 +5,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import {
Platform,
Text,
TouchableHighlight,
View
} from 'react-native';
@ -13,22 +12,17 @@ import {injectIntl, intlShape} from 'react-intl';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import Badge from 'app/components/badge';
import SearchBar from 'app/components/search_bar';
import {preventDoubleTap} from 'app/utils/tap';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FilteredList from './filtered_list';
import List from './list';
import SwitchTeams from './switch_teams';
class ChannelsList extends React.PureComponent {
static propTypes = {
channels: PropTypes.object.isRequired,
channelMembers: PropTypes.object,
currentChannel: PropTypes.object,
currentTeam: PropTypes.object.isRequired,
intl: intlShape.isRequired,
myTeamMembers: PropTypes.object.isRequired,
navigator: PropTypes.object,
onJoinChannel: PropTypes.func.isRequired,
onSearchEnds: PropTypes.func.isRequired,
@ -38,11 +32,6 @@ class ChannelsList extends React.PureComponent {
theme: PropTypes.object.isRequired
};
static defaultProps = {
currentTeam: {},
currentChannel: {}
};
constructor(props) {
super(props);
this.firstUnreadChannel = null;
@ -66,7 +55,7 @@ class ChannelsList extends React.PureComponent {
this.refs.search_bar.cancel();
};
openSettingsModal = () => {
openSettingsModal = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@ -88,7 +77,7 @@ class ChannelsList extends React.PureComponent {
}]
}
});
};
});
onSearch = (term) => {
this.setState({term});
@ -107,33 +96,30 @@ class ChannelsList extends React.PureComponent {
render() {
const {
currentChannel,
currentTeam,
intl,
myTeamMembers,
navigator,
onShowTeams,
theme
} = this.props;
if (!currentChannel) {
return <Text>{'Loading'}</Text>;
}
const {searching, term} = this.state;
const teamMembers = Object.values(myTeamMembers);
const showMembers = teamMembers.length > 1;
const styles = getStyleSheet(theme);
let settings;
let list;
if (searching) {
const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles, term};
list = <FilteredList {...listProps}/>;
list = (
<FilteredList
onSelectChannel={this.onSelectChannel}
styles={styles}
term={term}
/>
);
} else {
settings = (
<TouchableHighlight
style={styles.settingsContainer}
onPress={() => preventDoubleTap(this.openSettingsModal)}
onPress={this.openSettingsModal}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<AwesomeIcon
@ -143,8 +129,13 @@ class ChannelsList extends React.PureComponent {
</TouchableHighlight>
);
const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles};
list = <List {...listProps}/>;
list = (
<List
navigator={navigator}
onSelectChannel={this.onSelectChannel}
styles={styles}
/>
);
}
const title = (
@ -174,67 +165,18 @@ class ChannelsList extends React.PureComponent {
</View>
);
let badge;
let switcher;
if (showMembers && !searching) {
let mentionCount = 0;
let messageCount = 0;
teamMembers.forEach((m) => {
if (m.team_id !== currentTeam.id) {
mentionCount = mentionCount + (m.mention_count || 0);
messageCount = messageCount + (m.msg_count || 0);
}
});
let badgeCount = 0;
if (mentionCount) {
badgeCount = mentionCount;
} else if (messageCount) {
badgeCount = -1;
}
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
switcher = (
<TouchableHighlight
onPress={() => preventDoubleTap(onShowTeams)}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<View style={styles.switcherContainer}>
<AwesomeIcon
name='chevron-left'
size={12}
color={theme.sidebarHeaderBg}
/>
<View style={styles.switcherDivider}/>
<Text style={styles.switcherTeam}>
{currentTeam.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
</TouchableHighlight>
);
}
return (
<View
style={[styles.container, showMembers ? styles.extraPadding : {}]}
style={styles.container}
>
<View style={styles.statusBar}>
<View style={styles.headerContainer}>
{switcher}
<SwitchTeams
searching={searching}
showTeams={onShowTeams}
/>
{title}
{settings}
{badge}
</View>
</View>
{list}
@ -249,9 +191,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
backgroundColor: theme.sidebarBg,
flex: 1
},
extraPadding: {
paddingBottom: 5
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
@ -303,7 +242,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontSize: 18,
fontWeight: '300'
},
titleContainer: {
titleContainer: { // These aren't used by this component, but they are passed down to the list component
alignItems: 'center',
flex: 1,
flexDirection: 'row',
@ -330,43 +269,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
}
})
},
switcherContainer: {
alignItems: 'center',
backgroundColor: theme.sidebarHeaderTextColor,
borderRadius: 2,
flexDirection: 'row',
height: 32,
justifyContent: 'center',
marginLeft: 6,
marginRight: 10,
paddingHorizontal: 6
},
switcherDivider: {
backgroundColor: theme.sidebarHeaderBg,
height: 15,
marginHorizontal: 6,
width: 1
},
switcherTeam: {
color: theme.sidebarHeaderBg,
fontFamily: 'OpenSans',
fontSize: 14
},
badge: {
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
flexDirection: 'row',
padding: 3,
position: 'absolute',
left: 5,
top: 0
},
mention: {
color: theme.mentionColor,
fontSize: 10
},
divider: {
backgroundColor: changeOpacity(theme.sidebarText, 0.1),
height: 1

View file

@ -13,7 +13,6 @@ import {
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
import {General} from 'mattermost-redux/constants';
@ -34,7 +33,6 @@ class FilteredList extends Component {
currentTeam: PropTypes.object.isRequired,
currentUserId: PropTypes.string,
currentChannel: PropTypes.object,
groupChannels: PropTypes.array,
groupChannelMemberDetails: PropTypes.object,
intl: intlShape.isRequired,
teammateNameDisplay: PropTypes.string,
@ -325,7 +323,7 @@ class FilteredList extends Component {
return (
<TouchableHighlight
style={styles.actionContainer}
onPress={() => preventDoubleTap(action, this)}
onPress={action}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
>
<MaterialIcon

View file

@ -9,10 +9,15 @@ import {searchChannels} from 'mattermost-redux/actions/channels';
import {getProfilesInTeam, searchProfiles} from 'mattermost-redux/actions/users';
import {makeGroupMessageVisibleIfNecessary} from 'mattermost-redux/actions/preferences';
import {General} from 'mattermost-redux/constants';
import {getGroupChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels';
import {
getChannelsWithUnreadSection,
getCurrentChannel,
getGroupChannels,
getOtherChannels
} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId, getProfilesInCurrentTeam, getUsers, getUserIdsInChannels, getUserStatuses} from 'mattermost-redux/selectors/entities/users';
import {getDirectShowPreferences, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getDirectShowPreferences, getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import Config from 'assets/config';
@ -102,9 +107,10 @@ function mapStateToProps(state, ownProps) {
const searchOrder = Config.DrawerSearchOrder ? Config.DrawerSearchOrder : DEFAULT_SEARCH_ORDER;
return {
channels: getChannelsWithUnreadSection(state),
currentChannel: getCurrentChannel(state),
currentUserId,
otherChannels: getOtherChannels(state),
groupChannels: getGroupChannels(state),
groupChannelMemberDetails: getGroupChannelMemberDetails(state),
profiles,
teamProfiles,
@ -113,6 +119,7 @@ function mapStateToProps(state, ownProps) {
searchOrder,
pastDirectMessages: pastDirectMessages(state),
restrictDms,
theme: getTheme(state),
...ownProps
};
}

View file

@ -5,19 +5,11 @@ import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import {getChannelsWithUnreadSection, getCurrentChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import ChannelsList from './channels_list';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
channels: getChannelsWithUnreadSection(state),
channelMembers: getMyChannelMemberships(state),
currentChannel: getCurrentChannel(state),
currentTeam: getCurrentTeam(state),
myTeamMembers: getTeamMemberships(state),
theme: getTheme(state)
};
}

View file

@ -4,7 +4,9 @@
import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
import {getChannelsWithUnreadSection, getCurrentChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
@ -16,6 +18,10 @@ function mapStateToProps(state, ownProps) {
return {
canCreatePrivateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)),
channelMembers: getMyChannelMemberships(state),
channels: getChannelsWithUnreadSection(state),
currentChannel: getCurrentChannel(state),
theme: getTheme(state),
...ownProps
};
}

View file

@ -14,7 +14,7 @@ import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {preventDoubleTap} from 'app/utils/tap';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
import {General} from 'mattermost-redux/constants';
@ -151,7 +151,7 @@ class List extends Component {
);
};
createPrivateChannel = () => {
createPrivateChannel = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@ -171,7 +171,7 @@ class List extends Component {
closeButton: this.closeButton
}
});
};
});
buildChannels = (props) => {
const {canCreatePrivateChannels, styles} = props;
@ -233,7 +233,15 @@ class List extends Component {
return data;
};
showDirectMessagesModal = () => {
scrollToTop = () => {
this.refs.list.scrollToOffset({
x: 0,
y: 0,
animated: true
});
}
showDirectMessagesModal = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@ -255,9 +263,9 @@ class List extends Component {
}]
}
});
};
});
showMoreChannelsModal = () => {
showMoreChannelsModal = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@ -276,14 +284,14 @@ class List extends Component {
closeButton: this.closeButton
}
});
};
});
renderSectionAction = (styles, action) => {
const {theme} = this.props;
return (
<TouchableHighlight
style={styles.actionContainer}
onPress={() => preventDoubleTap(action, this)}
onPress={action}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
>
<MaterialIcon
@ -340,7 +348,7 @@ class List extends Component {
above = (
<UnreadIndicator
style={[styles.above, {width: (this.width - 40)}]}
onPress={() => this.refs.list.scrollToOffset({x: 0, y: 0, animated: true})}
onPress={this.scrollToTop}
text={(
<FormattedText
style={styles.indicatorText}

View file

@ -0,0 +1,20 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import SwitchTeams from './switch_teams';
function mapStateToProps(state, ownProps) {
return {
currentTeam: getCurrentTeam(state),
teamMembers: getTeamMemberships(state),
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(SwitchTeams);

View file

@ -0,0 +1,168 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {
Text,
TouchableHighlight,
View
} from 'react-native';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import Badge from 'app/components/badge';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SwitchTeams extends React.PureComponent {
static propTypes = {
currentTeam: PropTypes.object.isRequired,
searching: PropTypes.bool.isRequired,
showTeams: PropTypes.func.isRequired,
teamMembers: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.state = {
badgeCount: this.getBadgeCount(props)
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentTeam !== this.props.currentTeam || nextProps.teamMembers !== this.props.teamMembers) {
this.setState({
badgeCount: this.getBadgeCount(nextProps)
});
}
}
getBadgeCount = (props) => {
const {
currentTeam,
teamMembers
} = props;
let mentionCount = 0;
let messageCount = 0;
Object.values(teamMembers).forEach((m) => {
if (m.team_id !== currentTeam.id) {
mentionCount = mentionCount + (m.mention_count || 0);
messageCount = messageCount + (m.msg_count || 0);
}
});
let badgeCount;
if (mentionCount) {
badgeCount = mentionCount;
} else if (messageCount) {
badgeCount = -1;
} else {
badgeCount = 0;
}
return badgeCount;
};
showTeams = wrapWithPreventDoubleTap(() => {
this.props.showTeams();
});
render() {
const {
currentTeam,
searching,
teamMembers,
theme
} = this.props;
const {
badgeCount
} = this.state;
if (searching || teamMembers.length < 2) {
return null;
}
const styles = getStyleSheet(theme);
let badge;
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
return (
<View>
<TouchableHighlight
onPress={this.showTeams}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<View style={styles.switcherContainer}>
<AwesomeIcon
name='chevron-left'
size={12}
color={theme.sidebarHeaderBg}
/>
<View style={styles.switcherDivider}/>
<Text style={styles.switcherTeam}>
{currentTeam.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
</TouchableHighlight>
{badge}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
switcherContainer: {
alignItems: 'center',
backgroundColor: theme.sidebarHeaderTextColor,
borderRadius: 2,
flexDirection: 'row',
height: 32,
justifyContent: 'center',
marginLeft: 6,
marginRight: 10,
paddingHorizontal: 6
},
switcherDivider: {
backgroundColor: theme.sidebarHeaderBg,
height: 15,
marginHorizontal: 6,
width: 1
},
switcherTeam: {
color: theme.sidebarHeaderBg,
fontFamily: 'OpenSans',
fontSize: 14
},
badge: {
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
flexDirection: 'row',
padding: 3,
position: 'absolute',
left: -5,
top: -5
},
mention: {
color: theme.mentionColor,
fontSize: 10
}
};
});

View file

@ -35,6 +35,7 @@ export default class PostList extends PureComponent {
loadMore: PropTypes.func,
navigator: PropTypes.object,
onPostPress: PropTypes.func,
onRefresh: PropTypes.func,
posts: PropTypes.array.isRequired,
refreshing: PropTypes.bool,
renderReplies: PropTypes.bool,
@ -78,11 +79,19 @@ export default class PostList extends PureComponent {
};
onRefresh = () => {
const {actions, channel} = this.props;
const {
actions,
channel,
onRefresh
} = this.props;
if (Object.keys(channel).length) {
actions.refreshChannelWithRetry(channel.id);
}
if (onRefresh) {
onRefresh();
}
};
renderChannelIntro = () => {

View file

@ -2,26 +2,19 @@
// See License.txt for license information.
import {connect} from 'react-redux';
import DeviceInfo from 'react-native-device-info';
import {Client, Client4} from 'mattermost-redux/client';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getCurrentLocale} from 'app/selectors/i18n';
import {getTheme} from 'app/selectors/preferences';
import {removeProtocol} from 'app/utils/url';
import Root from './root';
function mapStateToProps(state, ownProps) {
const users = state.entities.users;
const {currentUserId} = users;
let locale = DeviceInfo.getDeviceLocale().split('-')[0];
if (currentUserId && users.profiles[currentUserId]) {
locale = users.profiles[currentUserId].locale;
}
const locale = getCurrentLocale(state);
Client.setLocale(locale);
Client4.setAcceptLanguage(locale);

View file

@ -1,12 +1,12 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import {InteractionManager, Keyboard} from 'react-native';
import PropTypes from 'prop-types';
import Search from './search_box';
export default class SearchBarIos extends Component {
export default class SearchBarIos extends PureComponent {
static propTypes = {
onCancelButtonPress: PropTypes.func,
onChangeText: PropTypes.func,

View file

@ -34,6 +34,7 @@ const ViewTypes = keyMirror({
ADD_FILE_TO_FETCH_CACHE: null,
SET_CHANNEL_LOADER: null,
SET_CHANNEL_REFRESHING: null,
SET_CHANNEL_DISPLAY_NAME: null,
POST_TOOLTIP_VISIBLE: null,

View file

@ -32,7 +32,7 @@ import ru from 'assets/i18n/ru.json';
import zhCN from 'assets/i18n/zh-CN.json';
import zhTW from 'assets/i18n/zh-TW.json';
const DEFAULT_LOCALE = 'en';
export const DEFAULT_LOCALE = 'en';
const TRANSLATIONS = {
de,

View file

@ -2,7 +2,11 @@
// See License.txt for license information.
import {combineReducers} from 'redux';
import {ChannelTypes, FileTypes, PostTypes} from 'mattermost-redux/action_types';
import {
ChannelTypes,
FileTypes,
PostTypes
} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
@ -181,6 +185,18 @@ function loading(state = false, action) {
}
}
function refreshing(state = false, action) {
switch (action.type) {
case PostTypes.GET_POSTS_SUCCESS:
case PostTypes.GET_POSTS_FAILURE:
return false;
case ViewTypes.SET_CHANNEL_REFRESHING:
return action.loading;
default:
return state;
}
}
function tooltipVisible(state = false, action) {
switch (action.type) {
case ViewTypes.POST_TOOLTIP_VISIBLE:
@ -236,6 +252,7 @@ export default combineReducers({
displayName,
drafts,
loading,
refreshing,
tooltipVisible,
postVisibility,
loadingPosts

View file

@ -51,7 +51,9 @@ class ChannelPostList extends PureComponent {
super(props);
this.state = {
retryMessageHeight: new Animated.Value(0)
retryMessageHeight: new Animated.Value(0),
visiblePosts: this.getVisiblePosts(props),
showLoadMore: false
};
}
@ -83,12 +85,22 @@ class ChannelPostList extends PureComponent {
this.setState({
showLoadMore
});
if (nextProps.posts !== this.props.posts || nextProps.postVisibility !== this.props.postVisibility) {
this.setState({
visiblePosts: this.getVisiblePosts(nextProps)
});
}
}
componentWillUnmount() {
this.mounted = false;
}
getVisiblePosts = (props) => {
return props.posts.slice(0, props.posts.postVisibility);
}
shouldMarkChannelAsLoaded = (postsCount, channelHasMessages, channelRefreshingFailed) => {
if (postsCount || channelHasMessages || channelRefreshingFailed) {
this.channelLoaded();
@ -159,8 +171,13 @@ class ChannelPostList extends PureComponent {
this.props.actions.loadPostsIfNecessaryWithRetry(channelId);
};
loadPostsRetry = () => {
this.loadPosts(this.props.channel.id);
};
render() {
const {
actions,
channel,
channelIsLoading,
channelIsRefreshing,
@ -169,17 +186,20 @@ class ChannelPostList extends PureComponent {
myMember,
navigator,
posts,
postVisibility,
theme
} = this.props;
const {retryMessageHeight} = this.state;
const {
retryMessageHeight,
showLoadMore,
visiblePosts
} = this.state;
let component;
if (!posts.length && channelRefreshingFailed) {
component = (
<PostListRetry
retry={() => this.loadPosts(channel.id)}
retry={this.loadPostsRetry}
theme={theme}
/>
);
@ -188,11 +208,12 @@ class ChannelPostList extends PureComponent {
} else {
component = (
<PostList
posts={posts.slice(0, postVisibility)}
posts={visiblePosts}
loadMore={this.loadMorePosts}
isLoadingMore={loadingPosts}
showLoadMore={this.state.showLoadMore}
showLoadMore={showLoadMore}
onPostPress={this.goToThread}
onRefresh={actions.setChannelRefreshing}
renderReplies={true}
indicateNewMessages={true}
currentUserId={myMember.user_id}
@ -210,7 +231,7 @@ class ChannelPostList extends PureComponent {
};
return (
<View style={{flex: 1}}>
<View style={style.container}>
{component}
<AnimatedView style={[style.refreshIndicator, refreshIndicatorDimensions]}>
<FormattedText
@ -225,6 +246,9 @@ class ChannelPostList extends PureComponent {
}
const style = StyleSheet.create({
container: {
flex: 1
},
refreshIndicator: {
alignItems: 'center',
backgroundColor: '#fb8000',

View file

@ -33,7 +33,7 @@ function makeMapStateToProps() {
getPostsStatus = getPostsSince.status;
}
let channelIsRefreshing = getPostsStatus === RequestStatus.STARTED;
let channelIsRefreshing = state.views.channel.refreshing;
let channelRefreshingFailed = getPostsStatus === RequestStatus.FAILURE && webSocketOnline;
if (!networkOnline) {
channelIsRefreshing = false;

31
app/selectors/i18n.js Normal file
View file

@ -0,0 +1,31 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import DeviceInfo from 'react-native-device-info';
import {createSelector} from 'reselect';
import DEFAULT_LOCALE from 'app/i18n';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
export const getCurrentUserLocale = createSelector(
getCurrentUser,
(currentUser) => {
return currentUser ? currentUser.locale : '';
}
);
// Not a proper selector since the device locale isn't in the redux store
export function getCurrentLocale(state) {
const userLocale = getCurrentUserLocale(state);
if (userLocale) {
return userLocale;
}
const deviceLocale = DeviceInfo.getDeviceLocale().split('-')[0];
if (deviceLocale) {
return deviceLocale;
}
return DEFAULT_LOCALE;
}

View file

@ -48,6 +48,26 @@ export default function configureAppStore(initialState) {
['typing']
);
const channelViewBlackList = {loading: true, refreshing: true, tooltipVisible: true, postVisibility: true, loadingPosts: true};
const channelViewBlackListFilter = createTransform(
(inboundState) => {
const channel = {};
for (const channelKey of Object.keys(inboundState.channel)) {
if (channelViewBlackList[channelKey]) {
channel[channelKey] = inboundState.channel[channelKey];
}
}
return {
...inboundState,
channel
};
},
null,
{whitelist: ['views']} // Only run this filter the views state (or any other entry that ends up being named views)
);
const setTransformer = createTransform(
(inboundState, key) => {
if (key === 'entities') {
@ -165,7 +185,8 @@ export default function configureAppStore(initialState) {
transforms: [
setTransformer,
viewsBlackListFilter,
typingBlackListFilter
typingBlackListFilter,
channelViewBlackListFilter
]
}
};