Reorganize sidebar channel items via pref (#1928)

* Reorganize sidebar channel items via pref
Add onItemPress


Add isModal prop


Add options modal for ungrouped sidebar


Filter zero item unreads from sections


Refactor unread filtering

Add SidebarSectionTypes to constants

Update mm-redux dep

Remove unused variable

* Fix lint errors. Update redux commit

* Add recent activity title

* Review feedback

* Fix issues after rebase

* Review feedback

* Change to use getAllDirectChannels

* Update redux library

* Fix lint error

* Update modal list snapshot
This commit is contained in:
Chris Duarte 2018-11-09 08:06:21 -08:00 committed by Elias Nahum
parent b80bbe3a94
commit 5c24375dae
12 changed files with 3392 additions and 3279 deletions

View file

@ -5,43 +5,64 @@ import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
import {
getFavoriteChannelIds,
getSortedUnreadChannelIds,
getSortedFavoriteChannelIds,
getSortedPublicChannelIds,
getSortedPrivateChannelIds,
getSortedDirectChannelIds,
getOrderedChannelIds,
} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTheme, getFavoritesPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme, getFavoritesPreferences, getSidebarPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
import {memoizeResult} from 'mattermost-redux/utils/helpers';
import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {SidebarSectionTypes} from 'app/constants/view';
import List from './list';
const filterZeroUnreads = memoizeResult((sections) => {
return sections.filter((s) => {
if (s.type === SidebarSectionTypes.UNREADS) {
return s.items.length > 0;
}
return true;
});
});
function mapStateToProps(state) {
const config = getConfig(state);
const license = getLicense(state);
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const unreadChannelIds = getSortedUnreadChannelIds(state);
const favoriteChannelIds = getSortedFavoriteChannelIds(state);
const privateChannelIds = getSortedPrivateChannelIds(state);
const directChannelIds = getSortedDirectChannelIds(state);
const currentTeamId = getCurrentTeamId(state);
const publicChannelIds = getSortedPublicChannelIds(state);
const isAdmin = checkIsAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
const sidebarPrefs = getSidebarPreferences(state);
const unreadChannelIds = getSortedUnreadChannelIds(state, null);
const favoriteChannelIds = getFavoriteChannelIds(state);
const orderedChannelIds = filterZeroUnreads(getOrderedChannelIds(
state,
null,
sidebarPrefs.grouping,
sidebarPrefs.sorting,
sidebarPrefs.unreads_at_top === 'true',
sidebarPrefs.favorite_at_top === 'true',
));
return {
canCreatePrivateChannels: showCreateOption(state, config, license, currentTeamId, General.PRIVATE_CHANNEL, isAdmin, isSystemAdmin),
unreadChannelIds,
canCreatePrivateChannels: showCreateOption(
state,
config,
license,
currentTeamId,
General.PRIVATE_CHANNEL,
isAdmin,
isSystemAdmin
),
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
directChannelIds,
theme: getTheme(state),
unreadChannelIds,
orderedChannelIds,
};
}

View file

@ -18,9 +18,9 @@ import {debounce} from 'mattermost-redux/actions/helpers';
import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item';
import {ListTypes} from 'app/constants';
import {SidebarSectionTypes} from 'app/constants/view';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
const VIEWABILITY_CONFIG = {
...ListTypes.VISIBILITY_CONFIG_DEFAULTS,
@ -32,15 +32,13 @@ let UnreadIndicator = null;
export default class List extends PureComponent {
static propTypes = {
canCreatePrivateChannels: PropTypes.bool.isRequired,
directChannelIds: PropTypes.array.isRequired,
favoriteChannelIds: PropTypes.array.isRequired,
navigator: PropTypes.object,
onSelectChannel: PropTypes.func.isRequired,
publicChannelIds: PropTypes.array.isRequired,
privateChannelIds: PropTypes.array.isRequired,
unreadChannelIds: PropTypes.array.isRequired,
styles: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
unreadChannelIds: PropTypes.array.isRequired,
orderedChannelIds: PropTypes.array.isRequired,
};
static contextTypes = {
@ -64,17 +62,13 @@ export default class List extends PureComponent {
componentWillReceiveProps(nextProps) {
const {
canCreatePrivateChannels,
directChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
orderedChannelIds,
unreadChannelIds,
} = this.props;
if (nextProps.canCreatePrivateChannels !== canCreatePrivateChannels ||
nextProps.directChannelIds !== directChannelIds || nextProps.favoriteChannelIds !== favoriteChannelIds ||
nextProps.publicChannelIds !== publicChannelIds || nextProps.privateChannelIds !== privateChannelIds ||
nextProps.unreadChannelIds !== unreadChannelIds) {
nextProps.unreadChannelIds !== unreadChannelIds ||
nextProps.orderedChannelIds !== orderedChannelIds) {
this.setState({sections: this.buildSections(nextProps)});
}
}
@ -88,68 +82,156 @@ export default class List extends PureComponent {
}
}
getSectionConfigByType = (sectionType) => {
const {canCreatePrivateChannels} = this.props;
switch (sectionType) {
case SidebarSectionTypes.UNREADS:
return {
id: 'mobile.channel_list.unreads',
defaultMessage: 'UNREADS',
};
case SidebarSectionTypes.FAVORITE:
return {
id: 'sidebar.favorite',
defaultMessage: 'FAVORITES',
};
case SidebarSectionTypes.PUBLIC:
return {
action: this.goToMoreChannels,
id: 'sidebar.channels',
defaultMessage: 'PUBLIC CHANNELS',
};
case SidebarSectionTypes.PRIVATE:
return {
action: canCreatePrivateChannels ? this.goToCreatePrivateChannel : null,
id: 'sidebar.pg',
defaultMessage: 'PRIVATE CHANNELS',
};
case SidebarSectionTypes.DIRECT:
return {
action: this.goToDirectMessages,
id: 'sidebar.direct',
defaultMessage: 'DIRECT MESSAGES',
};
case SidebarSectionTypes.RECENT_ACTIVITY:
return {
action: this.showCreateChannelOptions,
id: 'sidebar.types.recent',
defaultMessage: 'RECENT ACTIVITY',
};
case SidebarSectionTypes.ALPHA:
return {
action: this.showCreateChannelOptions,
id: 'mobile.channel_list.channels',
defaultMessage: 'CHANNELS',
};
default:
return {
action: this.showCreateChannelOptions,
id: 'mobile.channel_list.channels',
defaultMessage: 'CHANNELS',
};
}
}
buildSections = (props) => {
const {
canCreatePrivateChannels,
directChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
unreadChannelIds,
orderedChannelIds,
} = props;
const sections = [];
if (unreadChannelIds.length) {
sections.push({
id: t('mobile.channel_list.unreads'),
defaultMessage: 'UNREADS',
data: unreadChannelIds,
renderItem: this.renderUnreadItem,
topSeparator: false,
bottomSeparator: true,
});
}
if (favoriteChannelIds.length) {
sections.push({
id: t('sidebar.favorite'),
defaultMessage: 'FAVORITES',
data: favoriteChannelIds,
topSeparator: unreadChannelIds.length > 0,
bottomSeparator: true,
});
}
sections.push({
action: this.goToMoreChannels,
id: t('sidebar.channels'),
defaultMessage: 'PUBLIC CHANNELS',
data: publicChannelIds,
topSeparator: favoriteChannelIds.length > 0 || unreadChannelIds.length > 0,
bottomSeparator: publicChannelIds.length > 0,
return orderedChannelIds.map((s, i) => {
return {
...this.getSectionConfigByType(s.type),
data: s.items,
topSeparator: i !== 0,
bottomSeparator: s.items.length > 0,
};
});
sections.push({
action: canCreatePrivateChannels ? this.goToCreatePrivateChannel : null,
id: t('sidebar.pg'),
defaultMessage: 'PRIVATE CHANNELS',
data: privateChannelIds,
topSeparator: true,
bottomSeparator: privateChannelIds.length > 0,
});
sections.push({
action: this.goToDirectMessages,
id: t('sidebar.direct'),
defaultMessage: 'DIRECT MESSAGES',
data: directChannelIds,
topSeparator: true,
bottomSeparator: directChannelIds.length > 0,
});
return sections;
};
showCreateChannelOptions = () => {
const {canCreatePrivateChannels, navigator} = this.props;
const items = [];
const moreChannels = {
action: this.goToMoreChannels,
text: {
id: 'more_channels.title',
defaultMessage: 'More Channels',
},
};
const createPublicChannel = {
action: this.goToCreatePublicChannel,
text: {
id: 'mobile.create_channel.public',
defaultMessage: 'New Public Channel',
},
};
const createPrivateChannel = {
action: this.goToCreatePrivateChannel,
text: {
id: 'mobile.create_channel.private',
defaultMessage: 'New Private Channel',
},
};
const newConversation = {
action: this.goToDirectMessages,
text: {
id: 'mobile.more_dms.title',
defaultMessage: 'New Conversation',
},
};
items.push(moreChannels, createPublicChannel);
if (canCreatePrivateChannels) {
items.push(createPrivateChannel);
}
items.push(newConversation);
navigator.showModal({
screen: 'OptionsModal',
title: '',
animationType: 'none',
passProps: {
items,
onItemPress: () => navigator.dismissModal({
animationType: 'none',
}),
},
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
modalPresentationStyle: 'overCurrentContext',
},
});
}
goToCreatePublicChannel = preventDoubleTap(() => {
const {navigator, theme} = this.props;
const {intl} = this.context;
navigator.showModal({
screen: 'CreateChannel',
animationType: 'slide-up',
title: intl.formatMessage({id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'}),
backButtonTitle: '',
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
passProps: {
channelType: General.OPEN_CHANNEL,
closeButton: this.closeButton,
},
});
});
goToCreatePrivateChannel = preventDoubleTap(() => {
const {navigator, theme} = this.props;
const {intl} = this.context;
@ -256,21 +338,13 @@ export default class List extends PureComponent {
};
renderItem = ({item}) => {
return (
<ChannelItem
channelId={item}
isFavorite={this.props.favoriteChannelIds.includes(item)}
navigator={this.props.navigator}
onSelectChannel={this.onSelectChannel}
/>
);
};
const {favoriteChannelIds, unreadChannelIds} = this.props;
renderUnreadItem = ({item}) => {
return (
<ChannelItem
channelId={item}
isUnread={true}
isUnread={unreadChannelIds.includes(item)}
isFavorite={favoriteChannelIds.includes(item)}
navigator={this.props.navigator}
onSelectChannel={this.onSelectChannel}
/>

View file

@ -9,6 +9,16 @@ export const UpgradeTypes = {
NO_UPGRADE: 'no_upgrade',
};
export const SidebarSectionTypes = {
UNREADS: 'unreads',
FAVORITE: 'favorite',
PUBLIC: 'public',
PRIVATE: 'private',
DIRECT: 'direct',
RECENT_ACTIVITY: 'recent',
ALPHA: 'alpha',
};
const ViewTypes = keyMirror({
DATA_CLEANUP: null,
SERVER_URL_CHANGED: null,

View file

@ -60,7 +60,7 @@ export default class CreateChannel extends PureComponent {
this.rightButton.title = context.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'});
if (props.channelType === General.PRIVATE_CHANNEL) {
if (props.closeButton) {
this.left = {...this.leftButton, icon: props.closeButton};
}
@ -154,7 +154,7 @@ export default class CreateChannel extends PureComponent {
if (event.type === 'NavBarButtonPress') {
switch (event.id) {
case 'close-new-channel':
this.close(this.props.channelType === General.OPEN_CHANNEL);
this.close(!this.props.closeButton);
break;
case 'create-channel':
this.onCreateChannel();

View file

@ -195,10 +195,12 @@ exports[`OptionModalList should match snapshot for iOS 1`] = `
</Component>
<Component
style={
Object {
"borderBottomColor": "rgba(0, 0, 0, 0.1)",
"borderBottomWidth": 1,
}
Array [
Object {
"borderBottomColor": "rgba(0, 0, 0, 0.1)",
"borderBottomWidth": 1,
},
]
}
>
<TouchableOpacity
@ -242,7 +244,11 @@ exports[`OptionModalList should match snapshot for iOS 1`] = `
</TouchableOpacity>
</Component>
<Component
style={false}
style={
Array [
false,
]
}
>
<TouchableOpacity
activeOpacity={0.2}

View file

@ -27,6 +27,7 @@ export default class OptionsModal extends PureComponent {
deviceWidth: PropTypes.number.isRequired,
navigator: PropTypes.object,
onCancelPress: PropTypes.func,
onItemPress: PropTypes.func,
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
@ -35,6 +36,7 @@ export default class OptionsModal extends PureComponent {
static defaultProps = {
onCancelPress: emptyFunction,
onItemPress: emptyFunction,
};
constructor(props) {
@ -76,6 +78,7 @@ export default class OptionsModal extends PureComponent {
render() {
const {
items,
onItemPress,
title,
} = this.props;
@ -86,6 +89,7 @@ export default class OptionsModal extends PureComponent {
<OptionsModalList
items={items}
onCancelPress={this.handleCancel}
onItemPress={onItemPress}
title={title}
/>
</AnimatedView>

View file

@ -18,6 +18,7 @@ export default class OptionsModalList extends PureComponent {
static propTypes = {
items: PropTypes.array.isRequired,
onCancelPress: PropTypes.func,
onItemPress: PropTypes.func,
};
handleCancelPress = preventDoubleTap(() => {
@ -26,6 +27,15 @@ export default class OptionsModalList extends PureComponent {
}
});
handleItemPress = preventDoubleTap((action) => {
this.props.onItemPress();
setTimeout(() => {
if (typeof action === 'function') {
action();
}
}, 100);
});
renderOptions = () => {
const {items} = this.props;
@ -48,7 +58,7 @@ export default class OptionsModalList extends PureComponent {
style={style.optionBorder}
>
<TouchableOpacity
onPress={preventDoubleTap(item.action)}
onPress={() => this.handleItemPress(item.action)}
style={style.option}
>
{textComponent}

View file

@ -18,6 +18,7 @@ export default class OptionsModalList extends PureComponent {
static propTypes = {
items: PropTypes.array.isRequired,
onCancelPress: PropTypes.func,
onItemPress: PropTypes.func,
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
@ -30,6 +31,15 @@ export default class OptionsModalList extends PureComponent {
}
});
handleItemPress = preventDoubleTap((action) => {
this.props.onItemPress();
setTimeout(() => {
if (typeof action === 'function') {
action();
}
}, 100);
});
renderOptions = () => {
const {items} = this.props;
@ -49,10 +59,10 @@ export default class OptionsModalList extends PureComponent {
return (
<View
key={index}
style={(index < items.length - 1 && style.optionBorder)}
style={[(index < items.length - 1 && style.optionBorder)]}
>
<TouchableOpacity
onPress={preventDoubleTap(item.action)}
onPress={() => this.handleItemPress(item.action)}
style={style.option}
>
{textComponent}

View file

@ -475,6 +475,9 @@
"sidebar.direct": "DIRECT MESSAGES",
"sidebar.favorite": "FAVORITE CHANNELS",
"sidebar.pg": "PRIVATE CHANNELS",
"sidebar.types.recent": "RECENT ACTIVITY",
"sidebar.removeList": "Remove from list",
"sidebar.unreadSection": "UNREADS",
"sidebar.unreads": "More unreads",
"signup.email": "Email and Password",
"status_dropdown.set_away": "Away",

6319
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,7 @@
"intl": "1.2.5",
"jail-monkey": "1.0.0",
"jsc-android": "224109.1.0",
"mattermost-redux": "github:mattermost/mattermost-redux#9395b5287441a79c758edd0a7a5c0f6c188da0bf",
"mattermost-redux": "github:mattermost/mattermost-redux#e86e83dd44697d9a98cd5ab45be5b657340335f4",
"mime-db": "1.36.0",
"moment-timezone": "0.5.21",
"prop-types": "15.6.2",

View file

@ -14,7 +14,7 @@ import TableView from 'react-native-tableview';
import Icon from 'react-native-vector-icons/FontAwesome';
import {General} from 'mattermost-redux/constants';
import {getChannelsInTeam, getDirectChannels} from 'mattermost-redux/selectors/entities/channels';
import {getChannelsInTeam, getAllDirectChannels} from 'mattermost-redux/selectors/entities/channels';
import SearchBar from 'app/components/search_bar';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@ -111,7 +111,7 @@ export default class ExtensionChannels extends PureComponent {
// get the channels for the specified team
const channelsInTeam = getChannelsInTeam({entities});
const channelIds = channelsInTeam[teamId] || [];
const direct = getDirectChannels({entities});
const direct = getAllDirectChannels({entities});
const channels = channelIds.map((id) => this.props.entities.channels.channels[id]).concat(direct);
const icons = await Promise.all([