Channel Peek (#1203)

* Channel Peek

* feedback review
This commit is contained in:
enahum 2017-11-27 17:35:32 -03:00 committed by GitHub
parent 6b4100d517
commit f74b8e685d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 256 additions and 78 deletions

View file

@ -4,16 +4,21 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
Platform,
TouchableHighlight,
Text,
View
} from 'react-native';
import {intlShape} from 'react-intl';
import Badge from 'app/components/badge';
import ChannelIcon from 'app/components/channel_icon';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const {View: AnimatedView} = Animated;
export default class ChannelItem extends PureComponent {
static propTypes = {
channelId: PropTypes.string.isRequired,
@ -22,12 +27,17 @@ export default class ChannelItem extends PureComponent {
fake: PropTypes.bool,
isUnread: PropTypes.bool,
mentions: PropTypes.number.isRequired,
navigator: PropTypes.object,
onSelectChannel: PropTypes.func.isRequired,
status: PropTypes.string,
type: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
};
static contextTypes = {
intl: intlShape
};
onPress = wrapWithPreventDoubleTap(() => {
const {channelId, currentChannelId, displayName, fake, onSelectChannel, type} = this.props;
requestAnimationFrame(() => {
@ -35,6 +45,30 @@ export default class ChannelItem extends PureComponent {
});
});
onPreview = () => {
const {channelId, navigator} = this.props;
if (Platform.OS === 'ios' && navigator && this.previewRef) {
const {intl} = this.context;
navigator.push({
screen: 'ChannelPeek',
previewCommit: false,
previewView: this.previewRef,
previewActions: [{
id: 'action-mark-as-read',
title: intl.formatMessage({id: 'mobile.channel.markAsRead', defaultMessage: 'Mark As Read'})
}],
passProps: {
channelId
}
});
}
};
setPreviewRef = (ref) => {
this.previewRef = ref;
};
render() {
const {
channelId,
@ -93,25 +127,28 @@ export default class ChannelItem extends PureComponent {
);
return (
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.onPress}
>
<View style={style.container}>
{extraBorder}
<View style={[style.item, extraItemStyle]}>
{icon}
<Text
style={[style.text, extraTextStyle]}
ellipsizeMode='tail'
numberOfLines={1}
>
{displayName}
</Text>
{badge}
<AnimatedView ref={this.setPreviewRef}>
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.onPress}
onLongPress={this.onPreview}
>
<View style={style.container}>
{extraBorder}
<View style={[style.item, extraItemStyle]}>
{icon}
<Text
style={[style.text, extraTextStyle]}
ellipsizeMode='tail'
numberOfLines={1}
>
{displayName}
</Text>
{badge}
</View>
</View>
</View>
</TouchableHighlight>
</TouchableHighlight>
</AnimatedView>
);
}
}

View file

@ -10,7 +10,7 @@ import {
TouchableHighlight,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import {intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {General} from 'mattermost-redux/constants';
@ -21,12 +21,11 @@ import UnreadIndicator from 'app/components/channel_drawer/channels_list/unread_
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
class List extends PureComponent {
export default class List extends PureComponent {
static propTypes = {
canCreatePrivateChannels: PropTypes.bool.isRequired,
directChannelIds: PropTypes.array.isRequired,
favoriteChannelIds: PropTypes.array.isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object,
onSelectChannel: PropTypes.func.isRequired,
publicChannelIds: PropTypes.array.isRequired,
@ -36,6 +35,10 @@ class List extends PureComponent {
unreadChannelIds: PropTypes.array.isRequired
};
static contextTypes = {
intl: intlShape
};
constructor(props) {
super(props);
@ -140,7 +143,8 @@ class List extends PureComponent {
};
goToCreatePrivateChannel = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
const {navigator, theme} = this.props;
const {intl} = this.context;
navigator.showModal({
screen: 'CreateChannel',
@ -162,7 +166,8 @@ class List extends PureComponent {
});
goToDirectMessages = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
const {navigator, theme} = this.props;
const {intl} = this.context;
navigator.showModal({
screen: 'MoreDirectMessages',
@ -186,7 +191,8 @@ class List extends PureComponent {
});
goToMoreChannels = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
const {navigator, theme} = this.props;
const {intl} = this.context;
navigator.showModal({
screen: 'MoreChannels',
@ -245,6 +251,7 @@ class List extends PureComponent {
return (
<ChannelItem
channelId={item}
navigator={this.props.navigator}
onSelectChannel={this.onSelectChannel}
/>
);
@ -255,13 +262,15 @@ class List extends PureComponent {
<ChannelItem
channelId={item}
isUnread={true}
navigator={this.props.navigator}
onSelectChannel={this.onSelectChannel}
/>
);
};
renderSectionHeader = ({section}) => {
const {intl, styles} = this.props;
const {styles} = this.props;
const {intl} = this.context;
const {
action,
bottomSeparator,
@ -346,5 +355,3 @@ class List extends PureComponent {
);
}
}
export default injectIntl(List);

View file

@ -3,40 +3,45 @@
import {connect} from 'react-redux';
import {General, RequestStatus} from 'mattermost-redux/constants';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUser, getProfilesInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUser, makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelIntro from './channel_intro';
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state) || {};
const currentUser = getCurrentUser(state) || {};
const {status: getPostsRequestStatus} = state.requests.posts.getPosts;
function makeMapStateToProps() {
const getChannel = makeGetChannel();
const getProfilesInChannel = makeGetProfilesInChannel();
let currentChannelMembers = [];
if (currentChannel.type === General.DM_CHANNEL) {
const otherChannelMember = currentChannel.name.split('__').find((m) => m !== currentUser.id);
const otherProfile = state.entities.users.profiles[otherChannelMember];
if (otherProfile) {
currentChannelMembers.push(otherProfile);
return function mapStateToProps(state, ownProps) {
const currentChannel = getChannel(state, {id: ownProps.channelId}) || {};
const currentUser = getCurrentUser(state) || {};
const {status: getPostsRequestStatus} = state.requests.posts.getPosts;
let currentChannelMembers = [];
if (currentChannel.type === General.DM_CHANNEL) {
const otherChannelMember = currentChannel.name.split('__').find((m) => m !== currentUser.id);
const otherProfile = state.entities.users.profiles[otherChannelMember];
if (otherProfile) {
currentChannelMembers.push(otherProfile);
}
} else {
currentChannelMembers = getProfilesInChannel(state, ownProps.channelId) || [];
currentChannelMembers = currentChannelMembers.filter((m) => m.id !== currentUser.id);
}
} else {
currentChannelMembers = getProfilesInCurrentChannel(state) || [];
currentChannelMembers = currentChannelMembers.filter((m) => m.id !== currentUser.id);
}
const creator = currentChannel.creator_id === currentUser.id ? currentUser : state.entities.users.profiles[currentChannel.creator_id];
const postsInChannel = state.entities.posts.postsInChannel[currentChannel.id] || [];
const creator = currentChannel.creator_id === currentUser.id ? currentUser : state.entities.users.profiles[currentChannel.creator_id];
const postsInChannel = state.entities.posts.postsInChannel[ownProps.channelId] || [];
return {
creator,
currentChannel,
currentChannelMembers,
isLoadingPosts: !postsInChannel.length && getPostsRequestStatus === RequestStatus.STARTED,
theme: getTheme(state)
return {
creator,
currentChannel,
currentChannelMembers,
isLoadingPosts: !postsInChannel.length && getPostsRequestStatus === RequestStatus.STARTED,
theme: getTheme(state)
};
};
}
export default connect(mapStateToProps)(ChannelIntro);
export default connect(makeMapStateToProps)(ChannelIntro);

View file

@ -207,7 +207,12 @@ export default class PostList extends PureComponent {
);
}
return <ChannelIntro navigator={this.props.navigator}/>;
return (
<ChannelIntro
channelId={this.props.channelId}
navigator={this.props.navigator}
/>
);
};
render() {

View file

@ -0,0 +1,102 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import PostList from 'app/components/post_list';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelPeek extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
loadPostsIfNecessaryWithRetry: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired
}).isRequired,
channelId: PropTypes.string.isRequired,
currentUserId: PropTypes.string,
lastViewedAt: PropTypes.number,
navigator: PropTypes.object,
postIds: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired
};
static defaultProps = {
postVisibility: 15
};
constructor(props) {
super(props);
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
props.actions.loadPostsIfNecessaryWithRetry(props.channelId);
this.state = {
visiblePostIds: this.getVisiblePostIds(props)
};
}
componentWillReceiveProps(nextProps) {
const {postIds: nextPostIds} = nextProps;
let visiblePostIds = this.state.visiblePostIds;
if (nextPostIds !== this.props.postIds) {
visiblePostIds = this.getVisiblePostIds(nextProps);
}
this.setState({
visiblePostIds
});
}
getVisiblePostIds = (props) => {
return props.postIds.slice(0, 15);
};
onNavigatorEvent = (event) => {
if (event.type === 'PreviewActionPress') {
if (event.id === 'action-mark-as-read') {
const {actions, channelId} = this.props;
actions.markChannelAsRead(channelId);
}
}
};
render() {
const {
channelId,
currentUserId,
lastViewedAt,
navigator,
theme
} = this.props;
const {visiblePostIds} = this.state;
const style = getStyle(theme);
return (
<View style={style.container}>
<PostList
postIds={visiblePostIds}
showLoadMore={false}
renderReplies={true}
indicateNewMessages={true}
currentUserId={currentUserId}
lastViewedAt={lastViewedAt}
channelId={channelId}
navigator={navigator}
/>
</View>
);
}
}
const getStyle = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
};
});

View file

@ -0,0 +1,37 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getPostIdsInChannel} from 'mattermost-redux/selectors/entities/posts';
import {getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {loadPostsIfNecessaryWithRetry} from 'app/actions/views/channel';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelPeek from './channel_peek';
function mapStateToProps(state, ownProps) {
const channelId = ownProps.channelId;
return {
channelId,
currentUserId: getCurrentUserId(state),
postIds: getPostIdsInChannel(state, channelId),
lastViewedAt: getMyChannelMember(state, channelId).last_viewed_at,
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadPostsIfNecessaryWithRetry,
markChannelAsRead
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelPeek);

View file

@ -12,6 +12,7 @@ import Channel from 'app/screens/channel';
import ChannelAddMembers from 'app/screens/channel_add_members';
import ChannelInfo from 'app/screens/channel_info';
import ChannelMembers from 'app/screens/channel_members';
import ChannelPeek from 'app/screens/channel_peek';
import ClientUpgrade from 'app/screens/client_upgrade';
import Code from 'app/screens/code';
import CreateChannel from 'app/screens/create_channel';
@ -63,6 +64,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider);
Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(ChannelInfo), store, Provider);
Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(ChannelMembers), store, Provider);
Navigation.registerComponent('ChannelPeek', () => wrapWithContextProvider(ChannelPeek), store, Provider);
Navigation.registerComponent('ClientUpgrade', () => wrapWithContextProvider(ClientUpgrade), store, Provider);
Navigation.registerComponent('Code', () => wrapWithContextProvider(Code), store, Provider);
Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(CreateChannel), store, Provider);

View file

@ -1908,6 +1908,7 @@
"mobile.android.photos_permission_denied_title": "Photo library access is required",
"mobile.android.videos_permission_denied_description": "To upload videos from your library, please change your permission settings.",
"mobile.android.videos_permission_denied_title": "Video library access is required",
"mobile.channel.markAsRead": "Mark As Read",
"mobile.channel_drawer.search": "Jump to...",
"mobile.channel_info.alertMessageDeleteChannel": "Are you sure you want to delete the {term} {name}?",
"mobile.channel_info.alertMessageLeaveChannel": "Are you sure you want to leave the {term} {name}?",

View file

@ -9,4 +9,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 100c8053d4d12b6c3889d716c02d8b6e1840ed7c
COCOAPODS: 1.3.0
COCOAPODS: 1.3.1

View file

@ -34,7 +34,7 @@
"react-native-keyboard-aware-scroll-view": "0.4.1",
"react-native-linear-gradient": "2.3.0",
"react-native-local-auth": "enahum/react-native-local-auth.git",
"react-native-navigation": "1.1.260",
"react-native-navigation": "1.1.295",
"react-native-notifications": "enahum/react-native-notifications.git",
"react-native-orientation": "enahum/react-native-orientation.git",
"react-native-passcode-status": "1.1.0",

View file

@ -3959,27 +3959,9 @@ makeerror@1.0.x:
dependencies:
tmpl "1.0.x"
"mattermost-redux@file:../mattermost-redux":
mattermost-redux@mattermost/mattermost-redux#master:
version "1.0.1"
dependencies:
deep-equal "1.0.1"
form-data "2.3.1"
harmony-reflect "1.5.1"
isomorphic-fetch "2.2.1"
mime-db "1.30.0"
redux "3.7.2"
redux-action-buffer "1.1.0"
redux-batched-actions "0.2.0"
redux-offline "git+https://github.com/enahum/redux-offline.git"
redux-persist "4.9.1"
redux-thunk "2.2.0"
reselect "3.0.1"
serialize-error "2.1.0"
shallow-equals "1.0.0"
mattermost-redux@mattermost/mattermost-redux:
version "1.0.1"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/c293dff26e4db3cde91bf405677700c9fc6a58d8"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/4f36c293b136cdfb9f44b9536a5006084d0dc758"
dependencies:
deep-equal "1.0.1"
form-data "2.3.1"
@ -5137,9 +5119,9 @@ react-native-mock@0.3.1:
react-timer-mixin "^0.13.3"
warning "^2.1.0"
react-native-navigation@1.1.260:
version "1.1.260"
resolved "https://registry.yarnpkg.com/react-native-navigation/-/react-native-navigation-1.1.260.tgz#b886e23ab302f330145ecba94029807920421f97"
react-native-navigation@1.1.295:
version "1.1.295"
resolved "https://registry.yarnpkg.com/react-native-navigation/-/react-native-navigation-1.1.295.tgz#3ee6b2fc4f94671246498ed285f3592355c063d3"
dependencies:
lodash "4.x.x"