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:
parent
05bfc04ec9
commit
98abbd6dca
20 changed files with 380 additions and 173 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
31
app/selectors/i18n.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
]
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue