From f74b8e685dbc0ec6a4dbd5af20e7f09ecc55a28e Mon Sep 17 00:00:00 2001 From: enahum Date: Mon, 27 Nov 2017 17:35:32 -0300 Subject: [PATCH] Channel Peek (#1203) * Channel Peek * feedback review --- .../channel_item/channel_item.js | 73 +++++++++---- .../channel_drawer/channels_list/list/list.js | 25 +++-- app/components/channel_intro/index.js | 55 +++++----- app/components/post_list/post_list.js | 7 +- app/screens/channel_peek/channel_peek.js | 102 ++++++++++++++++++ app/screens/channel_peek/index.js | 37 +++++++ app/screens/index.js | 2 + assets/base/i18n/en.json | 1 + ios/Podfile.lock | 2 +- package.json | 2 +- yarn.lock | 28 +---- 11 files changed, 256 insertions(+), 78 deletions(-) create mode 100644 app/screens/channel_peek/channel_peek.js create mode 100644 app/screens/channel_peek/index.js diff --git a/app/components/channel_drawer/channels_list/channel_item/channel_item.js b/app/components/channel_drawer/channels_list/channel_item/channel_item.js index bd2bc46b4..261291e63 100644 --- a/app/components/channel_drawer/channels_list/channel_item/channel_item.js +++ b/app/components/channel_drawer/channels_list/channel_item/channel_item.js @@ -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 ( - - - {extraBorder} - - {icon} - - {displayName} - - {badge} + + + + {extraBorder} + + {icon} + + {displayName} + + {badge} + - - + + ); } } diff --git a/app/components/channel_drawer/channels_list/list/list.js b/app/components/channel_drawer/channels_list/list/list.js index 60edccd05..0acfbf46b 100644 --- a/app/components/channel_drawer/channels_list/list/list.js +++ b/app/components/channel_drawer/channels_list/list/list.js @@ -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 ( ); @@ -255,13 +262,15 @@ class List extends PureComponent { ); }; 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); diff --git a/app/components/channel_intro/index.js b/app/components/channel_intro/index.js index 78224f0d7..47a20cda6 100644 --- a/app/components/channel_intro/index.js +++ b/app/components/channel_intro/index.js @@ -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); diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 058ab2ff5..a84bf0bb9 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -207,7 +207,12 @@ export default class PostList extends PureComponent { ); } - return ; + return ( + + ); }; render() { diff --git a/app/screens/channel_peek/channel_peek.js b/app/screens/channel_peek/channel_peek.js new file mode 100644 index 000000000..6fab98887 --- /dev/null +++ b/app/screens/channel_peek/channel_peek.js @@ -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 ( + + + + ); + } +} + +const getStyle = makeStyleSheetFromTheme((theme) => { + return { + container: { + flex: 1, + backgroundColor: theme.centerChannelBg + } + }; +}); diff --git a/app/screens/channel_peek/index.js b/app/screens/channel_peek/index.js new file mode 100644 index 000000000..5184cfdab --- /dev/null +++ b/app/screens/channel_peek/index.js @@ -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); diff --git a/app/screens/index.js b/app/screens/index.js index 10d3ee475..5a719352a 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -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); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index be0b5398c..dcfa86a85 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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}?", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 537e7e042..90a858eae 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,4 +9,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 100c8053d4d12b6c3889d716c02d8b6e1840ed7c -COCOAPODS: 1.3.0 +COCOAPODS: 1.3.1 diff --git a/package.json b/package.json index 40b5393c5..19aabaaa4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index ab180ad24..90937c041 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"