diff --git a/NOTICE.txt b/NOTICE.txt index b6cd29986..1ae5f6e74 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -74,39 +74,6 @@ THE SOFTWARE. --- -## react-native-search-bar - -This product contains a modified portion of 'react-native-search-bar', a high-quality iOS native search bar for react native by Zhao Han. - -* HOMEPAGE: - * https://github.com/umhan35/react-native-search-bar - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2015-2016 Zhao Han - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - ## react-native-keyboard-aware-scroll This product contains 'react-native-keyboard-aware-scroll', a ScrollView component that handles keyboard appearance and automatically scrolls to focused `TextInput` diff --git a/app/components/channel_drawer/channel_drawer.js b/app/components/channel_drawer/channel_drawer.js index bb8b249dd..88fb46ce5 100644 --- a/app/components/channel_drawer/channel_drawer.js +++ b/app/components/channel_drawer/channel_drawer.js @@ -11,10 +11,12 @@ import { } from 'react-native'; import Drawer from 'app/components/drawer'; -import ChannelDrawerList from 'app/components/channel_drawer_list'; -import ChannelDrawerSwiper from 'app/components/channel_drawer_swiper'; -import ChannelDrawerTeams from 'app/components/channel_drawer_teams'; +import ChannelsList from './channels_list'; +import Swiper from './swiper'; +import TeamsList from './teams_list'; + +import {General} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; const DRAWER_INITIAL_OFFSET = 40; @@ -25,17 +27,19 @@ export default class ChannelDrawer extends PureComponent { getTeams: PropTypes.func.isRequired, handleSelectChannel: PropTypes.func.isRequired, viewChannel: PropTypes.func.isRequired, + makeDirectChannel: PropTypes.func.isRequired, markChannelAsRead: PropTypes.func.isRequired, setChannelLoading: PropTypes.func.isRequired }).isRequired, blurPostTextBox: PropTypes.func.isRequired, - navigator: PropTypes.object, children: PropTypes.node, - currentTeam: PropTypes.object, - currentChannel: PropTypes.object, channels: PropTypes.object, + currentChannel: PropTypes.object, channelMembers: PropTypes.object, + currentTeam: PropTypes.object, + currentUserId: PropTypes.string.isRequired, myTeamMembers: PropTypes.object.isRequired, + navigator: PropTypes.object, theme: PropTypes.object.isRequired }; @@ -93,7 +97,9 @@ export default class ChannelDrawer extends PureComponent { handleDrawerOpen = () => { this.setState({openDrawer: true}); - Keyboard.dismiss(); + if (this.state.openDrawerOffset === DRAWER_INITIAL_OFFSET) { + Keyboard.dismiss(); + } setTimeout(() => { InteractionManager.clearInteractionHandle(this.openLeftHandle); }); @@ -148,10 +154,58 @@ export default class ChannelDrawer extends PureComponent { }); }; + joinChannel = (channel) => { + const { + actions, + currentChannel, + currentTeam, + currentUserId + } = this.props; + + const { + handleSelectChannel, + joinChannel, + makeDirectChannel, + markChannelAsRead, + setChannelLoading, + viewChannel + } = actions; + + if (channel.type === General.DM_CHANNEL) { + markChannelAsRead(currentChannel.id); + setChannelLoading(); + viewChannel(currentChannel.id); + this.closeChannelDrawer(); + InteractionManager.runAfterInteractions(() => { + makeDirectChannel(channel.id); + }); + } else { + markChannelAsRead(currentChannel.id); + setChannelLoading(); + viewChannel(currentChannel.id); + joinChannel(currentUserId, currentTeam.id, channel.id); + this.closeChannelDrawer(); + InteractionManager.runAfterInteractions(() => { + handleSelectChannel(channel.id); + }); + } + }; + onPageSelected = (index) => { this.swiperIndex = index; }; + onSearchEnds = () => { + //hack to update the drawer when the offset changes + this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle + this.setState({openDrawerOffset: DRAWER_INITIAL_OFFSET}); + }; + + onSearchStart = () => { + this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle + this.setState({openDrawerOffset: 0}); + }; + showTeams = () => { const teamsCount = Object.keys(this.props.myTeamMembers).length; if (this.swiperIndex === 1 && teamsCount > 1) { @@ -160,7 +214,9 @@ export default class ChannelDrawer extends PureComponent { }; resetDrawer = () => { - this.refs.swiper.resetPage(); + if (this.swiperIndex !== 1) { + this.refs.swiper.resetPage(); + } }; renderContent = () => { @@ -173,13 +229,14 @@ export default class ChannelDrawer extends PureComponent { myTeamMembers, theme } = this.props; - const showTeams = Object.keys(myTeamMembers).length > 1; + const {openDrawerOffset} = this.state; + const showTeams = openDrawerOffset === DRAWER_INITIAL_OFFSET && Object.keys(myTeamMembers).length > 1; let teams; if (showTeams) { teams = ( - - ); return ( - {teams} {channelsList} - + ); }; @@ -224,6 +284,7 @@ export default class ChannelDrawer extends PureComponent { return ( { + searchProfiles(term); + searchChannels(currentTeam.id, term); + }, General.SEARCH_TIMEOUT_MILLISECONDS); + } + } + + onSelectChannel = (channel) => { + const { + makeGroupMessageVisibleIfNecessary, + setChannelDisplayName + } = this.props.actions; + + setChannelDisplayName(channel.display_name); + if (channel.type === General.GM_CHANNEL) { + makeGroupMessageVisibleIfNecessary(channel.id); + } + + this.props.onSelectChannel(channel); + }; + + createChannelElement = (channel) => { + return ( + + ); + }; + + filterChannels = (channels, term) => { + if (!term) { + return channels; + } + + const text = term.toLowerCase(); + return channels.filter((c) => { + return c.display_name.toLowerCase().includes(text); + }); + }; + + completeDirectGroupInfo = (channel) => { + const {currentUserId, myPreferences, profiles, profilesInChannel} = this.props; + const profilesIds = profilesInChannel[channel.id]; + if (profilesIds) { + function sortUsernames(a, b) { + const locale = profiles[currentUserId].locale; + return a.localeCompare(b, locale, {numeric: true}); + } + + const displayName = []; + profilesIds.forEach((teammateId) => { + if (teammateId !== currentUserId) { + displayName.push(displayUsername(profiles[teammateId], myPreferences)); + } + }); + + const gm = {...channel}; + return Object.assign(gm, { + display_name: displayName.sort(sortUsernames).join(', ') + }); + } + return channel; + }; + + buildChannelsForSearch = (props, term) => { + const data = []; + const {groupChannels, otherChannels, styles} = props; + const { + unreadChannels, + favoriteChannels, + publicChannels, + privateChannels + } = props.channels; + + const notMemberOf = otherChannels.map((o) => { + return { + ...o, + fake: true + }; + }); + + const favorites = favoriteChannels.filter((c) => { + return c.type !== General.DM_CHANNEL && c.type !== General.GM_CHANNEL; + }); + + const unreads = this.filterChannels(unreadChannels, term); + const channels = this.filterChannels([...favorites, ...publicChannels, ...privateChannels], term); + const others = this.filterChannels(notMemberOf, term); + const groups = this.filterChannels(groupChannels.map((g) => this.completeDirectGroupInfo(g)), term); + const fakeDms = this.filterChannels(this.buildFakeDms(props), term); + const directMessages = [...groups, ...fakeDms].sort(sortChannelsByDisplayName.bind(null, props.intl.locale)); + + if (unreads.length) { + data.push( + this.renderTitle(styles, 'mobile.channel_list.unreads', 'UNREADS', null, false, true), + ...unreads + ); + } + + if (channels.length) { + data.push( + this.renderTitle(styles, 'sidebar.channels', 'CHANNELS', null, unreads.length > 0, true), + ...channels + ); + } + + if (others.length) { + data.push( + this.renderTitle(styles, 'mobile.channel_list.not_member', 'NOT A MEMBER', null, channels.length > 0, true), + ...others + ); + } + + if (directMessages.length) { + data.push( + this.renderTitle(styles, 'sidebar.direct', 'DIRECT MESSAGES', null, others.length > 0, true), + ...directMessages + ); + } + + return data; + }; + + buildFakeDms = (props) => { + const {myPreferences, profiles, statuses} = props; + const users = Object.values(profiles); + + return users.map((u) => { + return { + id: u.id, + status: statuses[u.id], + display_name: displayUsername(u, myPreferences), + type: General.DM_CHANNEL, + fake: true + }; + }); + }; + + buildData = (props, term) => { + if (!props.currentChannel) { + return null; + } + + return this.buildChannelsForSearch(props, term); + }; + + renderSectionAction = (styles, action) => { + const {theme} = this.props; + return ( + preventDoubleTap(action, this)} + underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)} + > + + + ); + }; + + renderDivider = (styles, marginLeft) => { + return ( + + ); + }; + + renderItem = ({item}) => { + if (!item.isTitle) { + return this.createChannelElement(item); + } + return item.title; + }; + + renderTitle = (styles, id, defaultMessage, action, topDivider, bottomDivider) => { + const {formatMessage} = this.props.intl; + + return { + id, + isTitle: true, + title: ( + + {topDivider && this.renderDivider(styles, 0)} + + + {formatMessage({id, defaultMessage}).toUpperCase()} + + {action && this.renderSectionAction(styles, action)} + + {bottomDivider && this.renderDivider(styles, 16)} + + ) + }; + }; + + render() { + const {styles} = this.props; + + const {dataSource} = this.state; + + return ( + + item.id} + onViewableItemsChanged={this.updateUnreadIndicators} + keyboardDismissMode='on-drag' + maxToRenderPerBatch={10} + viewabilityConfig={{ + viewAreaCoveragePercentThreshold: 3, + waitForInteraction: false + }} + /> + + ); + } +} + +export default injectIntl(ChannelDrawerList); diff --git a/app/components/channel_drawer/channels_list/filtered_list/index.js b/app/components/channel_drawer/channels_list/filtered_list/index.js new file mode 100644 index 000000000..9a29afcff --- /dev/null +++ b/app/components/channel_drawer/channels_list/filtered_list/index.js @@ -0,0 +1,44 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {searchChannels} from 'mattermost-redux/actions/channels'; +import {searchProfiles} from 'mattermost-redux/actions/users'; +import {makeGroupMessageVisibleIfNecessary} from 'mattermost-redux/actions/preferences'; +import {getUserIdsInChannels, getUsers, getUserStatuses} from 'mattermost-redux/selectors/entities/users'; +import {getGroupChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels'; +import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences'; + +import {setChannelDisplayName} from 'app/actions/views/channel'; + +import FilteredList from './filtered_list'; + +function mapStateToProps(state, ownProps) { + const {currentUserId} = state.entities.users; + + return { + currentUserId, + otherChannels: getOtherChannels(state), + groupChannels: getGroupChannels(state), + profiles: getUsers(state), + profilesInChannel: getUserIdsInChannels(state), + myPreferences: getMyPreferences(state), + statuses: getUserStatuses(state), + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + makeGroupMessageVisibleIfNecessary, + searchChannels, + searchProfiles, + setChannelDisplayName + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(FilteredList); diff --git a/app/components/channel_drawer/channels_list/index.js b/app/components/channel_drawer/channels_list/index.js new file mode 100644 index 000000000..a198f3b01 --- /dev/null +++ b/app/components/channel_drawer/channels_list/index.js @@ -0,0 +1,396 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import { + Platform, + StyleSheet, + Text, + TouchableHighlight, + View +} from 'react-native'; +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 {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +import FilteredList from './filtered_list'; +import List from './list'; + +class ChannelsList extends Component { + static propTypes = { + channels: PropTypes.object.isRequired, + channelMembers: PropTypes.object, + currentTeam: PropTypes.object.isRequired, + currentChannel: PropTypes.object, + intl: intlShape.isRequired, + myTeamMembers: PropTypes.object.isRequired, + navigator: PropTypes.object, + onJoinChannel: PropTypes.func.isRequired, + onSearchEnds: PropTypes.func.isRequired, + onSearchStart: PropTypes.func.isRequired, + onSelectChannel: PropTypes.func.isRequired, + onShowTeams: PropTypes.func.isRequired, + theme: PropTypes.object.isRequired + }; + + static defaultProps = { + currentTeam: {}, + currentChannel: {} + }; + + constructor(props) { + super(props); + this.firstUnreadChannel = null; + this.state = { + searching: false, + term: null + }; + + MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor). + then((source) => { + this.closeButton = source; + }); + } + + onSelectChannel = (channel) => { + if (channel.fake) { + this.props.onJoinChannel(channel); + } else { + this.props.onSelectChannel(channel.id); + } + + this.refs.search_bar.cancel(); + }; + + openSettingsModal = () => { + const {intl, navigator, theme} = this.props; + + navigator.showModal({ + screen: 'Settings', + title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}), + animationType: 'slide-up', + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + }, + navigatorButtons: { + leftButtons: [{ + id: 'close-settings', + icon: this.closeButton + }] + } + }); + }; + + onSearch = (term) => { + this.setState({term}); + }; + + onSearchFocused = () => { + this.setState({searching: true}); + this.props.onSearchStart(); + }; + + cancelSearch = () => { + this.props.onSearchEnds(); + this.setState({searching: false}); + this.onSearch(null); + }; + + render() { + const { + currentChannel, + currentTeam, + intl, + myTeamMembers, + onShowTeams, + theme + } = this.props; + + if (!currentChannel) { + return {'Loading'}; + } + + const {searching, term} = this.state; + const teamMembers = Object.values(myTeamMembers); + const styles = getStyleSheet(theme); + + let settings; + let list; + if (searching) { + const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles, term}; + list = ; + } else { + settings = ( + preventDoubleTap(this.openSettingsModal)} + underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} + > + + + ); + + const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles}; + list = ; + } + + const title = ( + + + + ); + + let badge; + let switcher; + if (teamMembers.length > 1 && !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 = ( + + ); + } + + switcher = ( + preventDoubleTap(onShowTeams)} + underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} + > + + + + + {currentTeam.display_name.substr(0, 2).toUpperCase()} + + + + ); + } + + return ( + + + + {switcher} + {title} + {settings} + {badge} + + + {list} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + container: { + backgroundColor: theme.sidebarBg, + flex: 1 + }, + statusBar: { + backgroundColor: theme.sidebarHeaderBg, + ...Platform.select({ + ios: { + paddingTop: 20 + } + }) + }, + headerContainer: { + alignItems: 'center', + backgroundColor: theme.sidebarHeaderBg, + flexDirection: 'row', + borderBottomWidth: 1, + borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10), + ...Platform.select({ + android: { + height: 46 + }, + ios: { + height: 44 + } + }) + }, + header: { + color: theme.sidebarHeaderTextColor, + flex: 1, + fontSize: 17, + fontWeight: 'normal', + paddingLeft: 16 + }, + settingsContainer: { + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 10, + ...Platform.select({ + android: { + height: 46 + }, + ios: { + height: 44 + } + }) + }, + settings: { + color: theme.sidebarHeaderTextColor, + fontSize: 18, + fontWeight: '300' + }, + titleContainer: { + alignItems: 'center', + flex: 1, + flexDirection: 'row', + height: 48, + marginLeft: 16 + }, + title: { + flex: 1, + color: theme.sidebarText, + opacity: 1, + fontSize: 15, + fontWeight: '400', + letterSpacing: 0.8, + lineHeight: 18 + }, + searchContainer: { + flex: 1, + ...Platform.select({ + android: { + marginBottom: 1 + }, + ios: { + marginBottom: 3 + } + }) + }, + switcherContainer: { + alignItems: 'center', + backgroundColor: theme.sidebarHeaderTextColor, + borderRadius: 2, + flexDirection: 'row', + height: 32, + justifyContent: 'center', + marginLeft: 16, + 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', + height: 20, + padding: 3, + position: 'absolute', + left: 5, + top: 0, + width: 20 + }, + mention: { + color: theme.mentionColor, + fontSize: 10 + }, + divider: { + backgroundColor: changeOpacity(theme.sidebarText, 0.1), + height: 1 + }, + actionContainer: { + alignItems: 'center', + height: 48, + justifyContent: 'center', + width: 50 + }, + action: { + color: theme.sidebarText, + fontSize: 20, + fontWeight: '500', + lineHeight: 18 + }, + above: { + backgroundColor: theme.mentionBj, + top: 9 + }, + indicatorText: { + backgroundColor: 'transparent', + color: theme.mentionColor, + fontSize: 14, + paddingVertical: 2, + paddingHorizontal: 4, + textAlign: 'center', + textAlignVertical: 'center' + } + }); +}); + +export default injectIntl(ChannelsList); diff --git a/app/components/channel_drawer_list/index.js b/app/components/channel_drawer/channels_list/list/index.js similarity index 88% rename from app/components/channel_drawer_list/index.js rename to app/components/channel_drawer/channels_list/list/index.js index 3168119a7..518e87080 100644 --- a/app/components/channel_drawer_list/index.js +++ b/app/components/channel_drawer/channels_list/list/index.js @@ -11,7 +11,7 @@ import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils'; import {setChannelDisplayName} from 'app/actions/views/channel'; -import ChannelDrawerList from './channel_drawer_list'; +import List from './list'; function mapStateToProps(state, ownProps) { const {config, license} = state.entities.general; @@ -31,4 +31,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawerList); +export default connect(mapStateToProps, mapDispatchToProps)(List); diff --git a/app/components/channel_drawer_list/channel_drawer_list.js b/app/components/channel_drawer/channels_list/list/list.js similarity index 56% rename from app/components/channel_drawer_list/channel_drawer_list.js rename to app/components/channel_drawer/channels_list/list/list.js index dfd299dd9..7ced2d444 100644 --- a/app/components/channel_drawer_list/channel_drawer_list.js +++ b/app/components/channel_drawer/channels_list/list/list.js @@ -6,27 +6,23 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import { FlatList, - Platform, - StyleSheet, Text, TouchableHighlight, View } from 'react-native'; 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 FormattedText from 'app/components/formatted_text'; import {preventDoubleTap} from 'app/utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import {changeOpacity} from 'app/utils/theme'; import {General} from 'mattermost-redux/constants'; -import ChannelDrawerItem from './channel_drawer_item'; -import UnreadIndicator from './unread_indicator'; +import ChannelItem from 'app/components/channel_drawer/channels_list/channel_item'; +import UnreadIndicator from 'app/components/channel_drawer/channels_list/unread_indicator'; -class ChannelDrawerList extends Component { +class List extends Component { static propTypes = { actions: PropTypes.shape({ setChannelDisplayName: PropTypes.func.isRequired @@ -34,18 +30,15 @@ class ChannelDrawerList extends Component { canCreatePrivateChannels: PropTypes.bool.isRequired, channels: PropTypes.object.isRequired, channelMembers: PropTypes.object, - currentTeam: PropTypes.object.isRequired, currentChannel: PropTypes.object, intl: intlShape.isRequired, - myTeamMembers: PropTypes.object.isRequired, navigator: PropTypes.object, onSelectChannel: PropTypes.func.isRequired, - onShowTeams: PropTypes.func.isRequired, + styles: PropTypes.object.isRequired, theme: PropTypes.object.isRequired }; static defaultProps = { - currentTeam: {}, currentChannel: {} }; @@ -53,9 +46,10 @@ class ChannelDrawerList extends Component { super(props); this.firstUnreadChannel = null; this.state = { - showAbove: false, - dataSource: this.buildData(props) + dataSource: this.buildData(props), + showAbove: false }; + MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor). then((source) => { this.closeButton = source; @@ -102,7 +96,7 @@ class ChannelDrawerList extends Component { onSelectChannel = (channel) => { this.props.actions.setChannelDisplayName(channel.display_name); - this.props.onSelectChannel(channel.id); + this.props.onSelectChannel(channel); }; onLayout = (event) => { @@ -150,7 +144,7 @@ class ChannelDrawerList extends Component { const unread = msgCount > 0; return ( - { - const data = []; - - if (!props.currentChannel) { - return data; - } - - const {canCreatePrivateChannels, theme} = this.props; - const styles = getStyleSheet(theme); - + buildChannels = (props) => { + const {canCreatePrivateChannels, styles} = props; const { unreadChannels, favoriteChannels, @@ -202,16 +188,18 @@ class ChannelDrawerList extends Component { directAndGroupChannels } = props.channels; + const data = []; + if (unreadChannels.length) { data.push( - this.renderTitle(styles, 'mobile.channel_list.unreads', 'UNREADS', null, false, unreadChannels.length > 0), + this.renderTitle(styles, 'mobile.channel_list.unreads', 'UNREADS', null, false, true), ...unreadChannels ); } if (favoriteChannels.length) { data.push( - this.renderTitle(styles, 'sidebar.favorite', 'FAVORITES', null, unreadChannels.length > 0, favoriteChannels.length > 0), + this.renderTitle(styles, 'sidebar.favorite', 'FAVORITES', null, unreadChannels.length > 0, true), ...favoriteChannels ); } @@ -235,36 +223,21 @@ class ChannelDrawerList extends Component { ...directAndGroupChannels ); + return data; + }; + + buildData = (props) => { + if (!props.currentChannel) { + return null; + } + + const data = this.buildChannels(props); this.firstUnreadChannel = null; this.findUnreadChannels(data); return data; }; - openSettingsModal = () => { - const {intl, navigator, theme} = this.props; - - navigator.showModal({ - screen: 'Settings', - title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}), - animationType: 'slide-up', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg - }, - navigatorButtons: { - leftButtons: [{ - id: 'close-settings', - icon: this.closeButton - }] - } - }); - }; - showDirectMessagesModal = () => { const {intl, navigator, theme} = this.props; @@ -364,34 +337,9 @@ class ChannelDrawerList extends Component { }; render() { - const { - currentChannel, - currentTeam, - myTeamMembers, - onShowTeams, - theme - } = this.props; + const {styles} = this.props; const {dataSource, showAbove} = this.state; - const teamMembers = Object.values(myTeamMembers); - - if (!currentChannel) { - return {'Loading'}; - } - const styles = getStyleSheet(theme); - - const settings = ( - preventDoubleTap(this.openSettingsModal)} - underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} - > - - - ); let above; if (showAbove) { @@ -410,87 +358,18 @@ class ChannelDrawerList extends Component { ); } - const title = ( - - {currentTeam.display_name} - - ); - - let badge; - let switcher; - if (teamMembers.length > 1) { - 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 = ( - - ); - } - - switcher = ( - preventDoubleTap(onShowTeams)} - underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} - > - - - - - {currentTeam.display_name.substr(0, 2).toUpperCase()} - - - - ); - } - return ( - - - {switcher} - {title} - {settings} - {badge} - - item.id} onViewableItemsChanged={this.updateUnreadIndicators} + keyboardDismissMode='on-drag' maxToRenderPerBatch={10} viewabilityConfig={{ viewAreaCoveragePercentThreshold: 3, @@ -503,144 +382,4 @@ class ChannelDrawerList extends Component { } } -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return StyleSheet.create({ - container: { - backgroundColor: theme.sidebarBg, - flex: 1 - }, - statusBar: { - backgroundColor: theme.sidebarHeaderBg, - ...Platform.select({ - ios: { - paddingTop: 20 - } - }) - }, - headerContainer: { - alignItems: 'center', - backgroundColor: theme.sidebarHeaderBg, - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10), - ...Platform.select({ - android: { - height: 46 - }, - ios: { - height: 44 - } - }) - }, - header: { - color: theme.sidebarHeaderTextColor, - flex: 1, - fontSize: 17, - fontWeight: 'normal', - paddingLeft: 16 - }, - settingsContainer: { - alignItems: 'center', - justifyContent: 'center', - width: 50, - ...Platform.select({ - android: { - height: 46 - }, - ios: { - height: 44 - } - }) - }, - settings: { - color: theme.sidebarHeaderTextColor, - fontSize: 18, - fontWeight: '300' - }, - titleContainer: { - alignItems: 'center', - flex: 1, - flexDirection: 'row', - height: 48, - marginLeft: 16 - }, - title: { - flex: 1, - color: theme.sidebarText, - opacity: 1, - fontSize: 15, - fontWeight: '400', - letterSpacing: 0.8, - lineHeight: 18 - }, - switcherContainer: { - alignItems: 'center', - backgroundColor: theme.sidebarHeaderTextColor, - borderRadius: 2, - flexDirection: 'row', - height: 32, - justifyContent: 'center', - marginLeft: 16, - 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', - height: 20, - padding: 3, - position: 'absolute', - left: 5, - top: 0, - width: 20 - }, - mention: { - color: theme.mentionColor, - fontSize: 10 - }, - divider: { - backgroundColor: changeOpacity(theme.sidebarText, 0.1), - height: 1 - }, - actionContainer: { - alignItems: 'center', - height: 48, - justifyContent: 'center', - width: 50 - }, - action: { - color: theme.sidebarText, - fontSize: 20, - fontWeight: '500', - lineHeight: 18 - }, - above: { - backgroundColor: theme.mentionBj, - top: 79 - }, - indicatorText: { - backgroundColor: 'transparent', - color: theme.mentionColor, - fontSize: 14, - paddingVertical: 2, - paddingHorizontal: 4, - textAlign: 'center', - textAlignVertical: 'center' - } - }); -}); - -export default injectIntl(ChannelDrawerList); +export default injectIntl(List); diff --git a/app/components/channel_drawer_list/unread_indicator.js b/app/components/channel_drawer/channels_list/unread_indicator.js similarity index 100% rename from app/components/channel_drawer_list/unread_indicator.js rename to app/components/channel_drawer/channels_list/unread_indicator.js diff --git a/app/components/channel_drawer/index.js b/app/components/channel_drawer/index.js index 4a574183f..988461370 100644 --- a/app/components/channel_drawer/index.js +++ b/app/components/channel_drawer/index.js @@ -4,21 +4,25 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels'; +import {joinChannel, viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels'; import {getTeams} from 'mattermost-redux/actions/teams'; import {getChannelsWithUnreadSection, getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; import {handleSelectChannel, setChannelLoading} from 'app/actions/views/channel'; +import {makeDirectChannel} from 'app/actions/views/more_dms'; import {getTheme} from 'app/selectors/preferences'; import ChannelDrawer from './channel_drawer.js'; function mapStateToProps(state, ownProps) { + const {currentUserId} = state.entities.users; + return { ...ownProps, currentTeam: getCurrentTeam(state), currentChannel: getCurrentChannel(state), + currentUserId, channels: getChannelsWithUnreadSection(state), channelMembers: state.entities.channels.myMembers, myTeamMembers: getTeamMemberships(state), @@ -31,7 +35,9 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ getTeams, handleSelectChannel, + joinChannel, viewChannel, + makeDirectChannel, markChannelAsRead, setChannelLoading }, dispatch) diff --git a/app/components/channel_drawer_swiper/index.js b/app/components/channel_drawer/swiper/index.js similarity index 63% rename from app/components/channel_drawer_swiper/index.js rename to app/components/channel_drawer/swiper/index.js index 5cf0965d2..212b0eefc 100644 --- a/app/components/channel_drawer_swiper/index.js +++ b/app/components/channel_drawer/swiper/index.js @@ -2,5 +2,5 @@ // See License.txt for license information. // Used to leverage the platform specific components -import ChannelDrawerSwiper from './channel_drawer_swiper'; -export default ChannelDrawerSwiper; +import Swiper from './swiper'; +export default Swiper; diff --git a/app/components/channel_drawer_swiper/channel_drawer_swiper.android.js b/app/components/channel_drawer/swiper/swiper.android.js similarity index 95% rename from app/components/channel_drawer_swiper/channel_drawer_swiper.android.js rename to app/components/channel_drawer/swiper/swiper.android.js index 85d2de02d..8605dca98 100644 --- a/app/components/channel_drawer_swiper/channel_drawer_swiper.android.js +++ b/app/components/channel_drawer/swiper/swiper.android.js @@ -5,7 +5,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {ViewPagerAndroid} from 'react-native'; -export default class ChannelDrawerSwiper extends PureComponent { +export default class SwiperAndroid extends PureComponent { static propTypes = { children: PropTypes.node.isRequired, onPageSelected: PropTypes.func, diff --git a/app/components/channel_drawer_swiper/channel_drawer_swiper.ios.js b/app/components/channel_drawer/swiper/swiper.ios.js similarity index 94% rename from app/components/channel_drawer_swiper/channel_drawer_swiper.ios.js rename to app/components/channel_drawer/swiper/swiper.ios.js index d3d7bf055..d1566e7f8 100644 --- a/app/components/channel_drawer_swiper/channel_drawer_swiper.ios.js +++ b/app/components/channel_drawer/swiper/swiper.ios.js @@ -10,7 +10,7 @@ import {changeOpacity} from 'app/utils/theme'; const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window'); -export default class ChannelDrawerSwiper extends PureComponent { +export default class SwiperIos extends PureComponent { static propTypes = { children: PropTypes.node.isRequired, onPageSelected: PropTypes.func, @@ -61,6 +61,7 @@ export default class ChannelDrawerSwiper extends PureComponent { automaticallyAdjustContentInsets={true} scrollEnabled={showTeams} showsPagination={showTeams} + keyboardShouldPersistTaps={'always'} > {children} diff --git a/app/components/channel_drawer_teams/index.js b/app/components/channel_drawer/teams_list/index.js similarity index 92% rename from app/components/channel_drawer_teams/index.js rename to app/components/channel_drawer/teams_list/index.js index 7290775b9..7f75b0eae 100644 --- a/app/components/channel_drawer_teams/index.js +++ b/app/components/channel_drawer/teams_list/index.js @@ -14,7 +14,7 @@ import {handleTeamChange} from 'app/actions/views/select_team'; import {getTheme} from 'app/selectors/preferences'; import {removeProtocol} from 'app/utils/url'; -import ChannelDrawerTeams from './channel_drawer_teams'; +import TeamsList from './teams_list'; function mapStateToProps(state, ownProps) { const user = getCurrentUser(state); @@ -48,4 +48,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawerTeams); +export default connect(mapStateToProps, mapDispatchToProps)(TeamsList); diff --git a/app/components/channel_drawer_teams/channel_drawer_teams.js b/app/components/channel_drawer/teams_list/teams_list.js similarity index 99% rename from app/components/channel_drawer_teams/channel_drawer_teams.js rename to app/components/channel_drawer/teams_list/teams_list.js index ca13dea1f..e97cd3fb3 100644 --- a/app/components/channel_drawer_teams/channel_drawer_teams.js +++ b/app/components/channel_drawer/teams_list/teams_list.js @@ -21,7 +21,7 @@ import FormattedText from 'app/components/formatted_text'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -class ChannelDrawerTeams extends PureComponent { +class TeamsList extends PureComponent { static propTypes = { actions: PropTypes.shape({ handleTeamChange: PropTypes.func.isRequired, @@ -333,4 +333,4 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }); }); -export default injectIntl(ChannelDrawerTeams); +export default injectIntl(TeamsList); diff --git a/app/components/post_list/new_messages_divider.js b/app/components/post_list/new_messages_divider.js index f7276aa2d..1abca9e93 100644 --- a/app/components/post_list/new_messages_divider.js +++ b/app/components/post_list/new_messages_divider.js @@ -2,6 +2,7 @@ // See License.txt for license information. import React from 'react'; +import PropTypes from 'prop-types'; import { StyleSheet, View, @@ -9,14 +10,17 @@ import { } from 'react-native'; import FormattedText from 'app/components/formatted_text'; +import {makeStyleSheetFromTheme} from 'app/utils/theme'; function NewMessagesDivider(props) { + const style = getStyleFromTheme(props.theme); + return ( @@ -27,28 +31,30 @@ function NewMessagesDivider(props) { } NewMessagesDivider.propTypes = { - style: ViewPropTypes.style + style: ViewPropTypes.style, + theme: PropTypes.object }; -const style = StyleSheet.create({ - container: { - alignItems: 'center', - flexDirection: 'row', - height: 28 - }, - textContainer: { - marginHorizontal: 15 - }, - line: { - flex: 1, - height: StyleSheet.hairlineWidth, - backgroundColor: '#f80' - }, - text: { - fontSize: 14, - fontWeight: '600', - color: '#ffaf53' - } +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + container: { + alignItems: 'center', + flexDirection: 'row', + height: 28 + }, + textContainer: { + marginHorizontal: 15 + }, + line: { + flex: 1, + height: StyleSheet.hairlineWidth, + backgroundColor: theme.newMessageSeparator + }, + text: { + fontSize: 14, + color: theme.newMessageSeparator + } + }); }); export default NewMessagesDivider; diff --git a/app/components/search_bar/search_bar.android.js b/app/components/search_bar/search_bar.android.js index 6dc6bb5ab..fd881fe63 100644 --- a/app/components/search_bar/search_bar.android.js +++ b/app/components/search_bar/search_bar.android.js @@ -3,176 +3,225 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { + InteractionManager, Keyboard, TextInput, StyleSheet, View, - TouchableOpacity + TouchableWithoutFeedback } from 'react-native'; import Icon from 'react-native-vector-icons/MaterialIcons'; -const styles = StyleSheet.create({ - searchBar: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: 'transparent', - margin: 8 - }, - searchBarInput: { - flex: 1, - fontWeight: 'normal', - backgroundColor: 'transparent' - } -}); +import {changeOpacity} from 'app/utils/theme'; export default class SearchBarAndroid extends PureComponent { static propTypes = { - height: PropTypes.number.isRequired, - fontSize: PropTypes.number.isRequired, - text: PropTypes.string, - placeholder: PropTypes.string, - showCancelButton: PropTypes.bool, - textFieldBackgroundColor: PropTypes.string, - placeholderTextColor: PropTypes.string, - textColor: PropTypes.string, - onChange: PropTypes.func, + onCancelButtonPress: PropTypes.func, onChangeText: PropTypes.func, onFocus: PropTypes.func, - onBlur: PropTypes.func, onSearchButtonPress: PropTypes.func, - onCancelButtonPress: PropTypes.func + backgroundColor: PropTypes.string, + placeholderTextColor: PropTypes.string, + titleCancelColor: PropTypes.string, + tintColorSearch: PropTypes.string, + tintColorDelete: PropTypes.string, + inputStyle: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.object + ]), + placeholder: PropTypes.string, + returnKeyType: PropTypes.string, + keyboardType: PropTypes.string, + autoCapitalize: PropTypes.string, + inputHeight: PropTypes.number, + inputBorderRadius: PropTypes.number, + blurOnSubmit: PropTypes.bool }; static defaultProps = { + blurOnSubmit: false, placeholder: 'Search', showCancelButton: true, - placeholderTextColor: '#bdbdbd', - textColor: '#212121', + placeholderTextColor: changeOpacity('#000', 0.5), onSearchButtonPress: () => true, onCancelButtonPress: () => true, onChangeText: () => true, - onFocus: () => true, - onBlur: () => true + onFocus: () => true }; constructor(props) { super(props); this.state = { - isOnFocus: false, - value: this.props.text - }; - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onCancelButtonPress = this.onCancelButtonPress.bind(this); - this.onSearchButtonPress = this.onSearchButtonPress.bind(this); - this.onChangeText = this.onChangeText.bind(this); - } - - onSearchButtonPress() { - const onSearchButtonPress = this.props.onSearchButtonPress; - if (this.state.value) { - onSearchButtonPress(this.state.value); - } - } - - onCancelButtonPress() { - const onCancelButtonPress = this.props.onCancelButtonPress; - this.setState({ - isOnFocus: false, + isFocused: false, value: '' + }; + } + + cancel = () => { + this.onCancelButtonPress(); + }; + + onSearchButtonPress = () => { + const {onSearchButtonPress} = this.props; + const {value} = this.state; + + if (value && onSearchButtonPress) { + onSearchButtonPress(value); + } + }; + + onCancelButtonPress = () => { + const {onCancelButtonPress} = this.props; + + Keyboard.dismiss(); + InteractionManager.runAfterInteractions(() => { + this.setState({ + isFocused: false, + value: '' + }, () => { + if (onCancelButtonPress) { + onCancelButtonPress(); + } + }); }); + }; - onCancelButtonPress(); - Keyboard.dismiss(); - } - - onChangeText(value) { - const onChangeText = this.props.onChangeText; + onChangeText = (value) => { + const {onChangeText} = this.props; this.setState({value}); - onChangeText(value); - } + if (onChangeText) { + onChangeText(value); + } + }; - onFocus() { - const onFocus = this.props.onFocus; - this.setState({isOnFocus: true}); - onFocus(); - } + onFocus = () => { + const {onFocus} = this.props; - onBlur() { - const onBlur = this.props.onBlur; - this.setState({isOnFocus: false}); - onBlur(); - Keyboard.dismiss(); - } + this.setState({isFocused: true}); - blur = () => { - this.onBlur(); + if (onFocus) { + onFocus(); + } }; render() { const { - height, + autoCapitalize, + backgroundColor, + blurOnSubmit, + inputHeight, + inputStyle, + keyboardType, placeholder, - fontSize, placeholderTextColor, - textColor, - textFieldBackgroundColor + returnKeyType, + titleCancelColor, + tintColorDelete, + tintColorSearch } = this.props; + const {isFocused, value} = this.state; - const searchBarStyle = { - height: height + 10, - paddingLeft: height * 0.25, - backgroundColor: textFieldBackgroundColor + const inputNoBackground = { + ...inputStyle }; + Reflect.deleteProperty(inputNoBackground, 'backgroundColor'); - const inputStyle = { - paddingLeft: height * 0.5, - fontSize, - color: textColor - }; + let inputColor = styles.searchBarInput.backgroundColor; + if (inputStyle) { + inputColor = inputStyle.backgroundColor; + } else { + inputNoBackground.backgroundColor = '#fff'; + } return ( - {this.state.isOnFocus && this.props.showCancelButton ? - + + {isFocused ? + + + : - : - - } - - {this.state.isOnFocus && this.state.value ? - this.setState({value: ''})}> - - : null - } + {isFocused && value ? + this.onChangeText('')}> + + : null + } + ); } } + +const styles = StyleSheet.create({ + container: { + backgroundColor: 'grey', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + padding: 5 + }, + searchBar: { + flex: 1, + flexDirection: 'row', + backgroundColor: 'red', + alignItems: 'center' + }, + searchBarInput: { + flex: 1, + fontWeight: 'normal', + textAlignVertical: 'center', + padding: 0, + includeFontPadding: false + } +}); diff --git a/app/components/search_bar/search_bar.ios.js b/app/components/search_bar/search_bar.ios.js index 2d3df10a0..03c9ae066 100644 --- a/app/components/search_bar/search_bar.ios.js +++ b/app/components/search_bar/search_bar.ios.js @@ -1,96 +1,145 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import SearchBar from 'react-native-search-bar'; -export default class SearchBarIos extends PureComponent { +import React, {Component} from 'react'; +import {InteractionManager, Text, Keyboard} from 'react-native'; +import PropTypes from 'prop-types'; +import Search from 'react-native-search-box'; + +export default class SearchBarIos extends Component { static propTypes = { - barStyle: PropTypes.oneOf(['default', 'black']), - searchBarStyle: PropTypes.oneOf(['default', 'prominent', 'minimal']), - text: PropTypes.string, - placeholder: PropTypes.string, - showCancelButton: PropTypes.bool, - hideBackground: PropTypes.bool, - textFieldBackgroundColor: PropTypes.string, - placeholderTextColor: PropTypes.string, - textColor: PropTypes.string, - barTintColor: PropTypes.string, //color of the background container - tintColor: PropTypes.string, // color of the carret and the cancel button - onChange: PropTypes.func, + onCancelButtonPress: PropTypes.func, onChangeText: PropTypes.func, onFocus: PropTypes.func, - onBlur: PropTypes.func, onSearchButtonPress: PropTypes.func, - onCancelButtonPress: PropTypes.func - }; - - static defaultProps = { - barStyle: 'default', - searchBarStyle: 'minimal', - placeholder: 'Search', - showCancelButton: true, - hideBackground: true, - onFocus: () => true, - onBlur: () => true + backgroundColor: PropTypes.string, + placeholderTextColor: PropTypes.string, + titleCancelColor: PropTypes.string, + tintColorSearch: PropTypes.string, + tintColorDelete: PropTypes.string, + cancelButtonStyle: PropTypes.PropTypes.oneOfType([ + PropTypes.number, + PropTypes.object + ]), + inputStyle: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.object + ]), + placeholder: PropTypes.string, + cancelTitle: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object + ]), + returnKeyType: PropTypes.string, + keyboardType: PropTypes.string, + autoCapitalize: PropTypes.string, + inputHeight: PropTypes.number, + inputBorderRadius: PropTypes.number, + blurOnSubmit: PropTypes.bool }; constructor(props) { super(props); this.state = { - displayCancelButton: false + placeholderWidth: 0 }; } - onFocus = (event) => { - const onFocus = this.props.onFocus; - if (this.props.showCancelButton) { - this.setState({displayCancelButton: true}); - } + shouldComponentUpdate(nextProps, nextState) { + return nextState.placeholderWidth !== this.state.placeholderWidth; + } - onFocus(event); + afterDelete = () => { + return new Promise((resolve) => { + this.refs.search.focus(); + resolve(); + }); }; - onBlur = (event) => { - const onBlur = this.props.onBlur; - if (this.props.showCancelButton) { - this.setState({displayCancelButton: false}); - } - - onBlur(event); + cancel = () => { + this.refs.search.onCancel(); }; - blur = () => { - this.searchBar.blur(); + onCancel = () => { + return new Promise((resolve) => { + Keyboard.dismiss(); + InteractionManager.runAfterInteractions(() => { + if (this.props.onCancelButtonPress) { + this.props.onCancelButtonPress(); + } + resolve(); + }); + }); }; - searchBarRef = (ref) => { - this.searchBar = ref; + onChangeText = (text) => { + return new Promise((resolve) => { + if (this.props.onChangeText) { + this.props.onChangeText(text); + } + resolve(); + }); + }; + + onDelete = () => { + return new Promise((resolve) => { + if (this.props.onChangeText) { + this.props.onChangeText(''); + } + resolve(); + }); + }; + + onFocus = () => { + return new Promise((resolve) => { + if (this.props.onFocus) { + this.props.onFocus(); + } + resolve(); + }); + }; + + onSearch = (text) => { + return new Promise((resolve) => { + if (this.props.onSearchButtonPress) { + this.props.onSearchButtonPress(text); + } + resolve(); + }); }; render() { + if (this.state.placeholderWidth) { + return ( + + ); + } + return ( - + { + const placeholderWidth = (event.nativeEvent.layout.width / 2); + this.setState({placeholderWidth}); + }} + > + {this.props.placeholder} + ); } } diff --git a/app/i18n/index.js b/app/i18n/index.js index dc5c6d663..cb1d7c5ec 100644 --- a/app/i18n/index.js +++ b/app/i18n/index.js @@ -2,6 +2,19 @@ // See License.txt for license information. import 'intl'; +import {addLocaleData} from 'react-intl'; +import deLocaleData from 'react-intl/locale-data/de'; +import enLocaleData from 'react-intl/locale-data/en'; +import esLocaleData from 'react-intl/locale-data/es'; +import frLocaleData from 'react-intl/locale-data/fr'; +import jaLocaleData from 'react-intl/locale-data/ja'; +import koLocaleData from 'react-intl/locale-data/ko'; +import nlLocaleData from 'react-intl/locale-data/nl'; +import plLocaleData from 'react-intl/locale-data/pl'; +import ptLocaleData from 'react-intl/locale-data/pt'; +import trLocaleData from 'react-intl/locale-data/tr'; +import ruLocaleData from 'react-intl/locale-data/ru'; +import zhLocaleData from 'react-intl/locale-data/zh'; import de from 'assets/i18n/de.json'; import en from 'assets/i18n/en.json'; @@ -12,6 +25,7 @@ import ko from 'assets/i18n/ko.json'; import nl from 'assets/i18n/nl.json'; import pl from 'assets/i18n/pl.json'; import ptBR from 'assets/i18n/pt-BR.json'; +import tr from 'assets/i18n/tr.json'; import ru from 'assets/i18n/ru.json'; import zhCN from 'assets/i18n/zh-CN.json'; import zhTW from 'assets/i18n/zh-TW.json'; @@ -27,12 +41,28 @@ const TRANSLATIONS = { ko, nl, pl, - ptBR, + 'pt-BR': ptBR, + tr, ru, - zhCN, - zhTW + 'zh-CN': zhCN, + 'zh-TW': zhTW }; +addLocaleData([ + deLocaleData, + enLocaleData, + esLocaleData, + frLocaleData, + jaLocaleData, + koLocaleData, + nlLocaleData, + plLocaleData, + ptLocaleData, + trLocaleData, + ruLocaleData, + zhLocaleData +]); + export function getTranslations(locale) { return TRANSLATIONS[locale] || TRANSLATIONS[DEFAULT_LOCALE]; } diff --git a/app/mattermost.js b/app/mattermost.js index daf4fbadd..5b3460b1f 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -21,6 +21,7 @@ import {General} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {goToNotification, loadConfigAndLicense, queueNotification} from 'app/actions/views/root'; +import {setChannelDisplayName} from 'app/actions/views/channel'; import {NavigationTypes, ViewTypes} from 'app/constants'; import initialState from 'app/initial_state'; import PushNotifications from 'app/push_notifications'; @@ -40,6 +41,7 @@ export default class Mattermost { AppState.addEventListener('change', this.handleAppStateChange); EventEmitter.on(General.CONFIG_CHANGED, this.handleConfigChanged); EventEmitter.on(NavigationTypes.NAVIGATION_RESET, this.handleReset); + EventEmitter.on(General.DEFAULT_CHANNEL, this.handleResetDisplayName); this.handleAppStateChange(AppState.currentState); Client4.setUserAgent(DeviceInfo.getUserAgent()); @@ -78,6 +80,10 @@ export default class Mattermost { this.startApp('fade'); }; + handleResetDisplayName = (displayName) => { + store.dispatch(setChannelDisplayName(displayName)); + }; + handleVersionUpgrade = async () => { const {dispatch, getState} = store; diff --git a/app/screens/channel_add_members/channel_add_members.js b/app/screens/channel_add_members/channel_add_members.js index 4b975da40..64bc37035 100644 --- a/app/screens/channel_add_members/channel_add_members.js +++ b/app/screens/channel_add_members/channel_add_members.js @@ -6,7 +6,6 @@ import PropTypes from 'prop-types'; import {injectIntl, intlShape} from 'react-intl'; import { Alert, - Keyboard, Platform, InteractionManager, StyleSheet, @@ -74,10 +73,6 @@ class ChannelAddMembers extends PureComponent { } componentDidMount() { - if (Platform.OS === 'android') { - Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); - } - InteractionManager.runAfterInteractions(() => { this.props.actions.getProfilesNotInChannel(this.props.currentTeam.id, this.props.currentChannel.id, 0); this.props.actions.getTeamStats(this.props.currentTeam.id); @@ -86,12 +81,6 @@ class ChannelAddMembers extends PureComponent { this.emitCanAddMembers(false); } - componentWillUnmount() { - if (Platform.OS === 'android') { - Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); - } - } - componentWillReceiveProps(nextProps) { const {loadMoreRequestStatus} = this.props; if (loadMoreRequestStatus === RequestStatus.STARTED && @@ -170,10 +159,6 @@ class ChannelAddMembers extends PureComponent { actions.handleAddChannelMembers(currentChannel.id, membersToAdd); }; - handleAndroidKeyboard = () => { - this.onSearchButtonPress(); - }; - handleRowSelect = (id) => { const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]}); @@ -215,16 +200,8 @@ class ChannelAddMembers extends PureComponent { } }; - onSearchButtonPress = () => { - this.searchBar.blur(); - }; - - searchBarRef = (ref) => { - this.searchBar = ref; - }; - - searchProfiles = (event) => { - const term = event.nativeEvent.text.toLowerCase(); + searchProfiles = (text) => { + const term = text.toLowerCase(); const {actions, currentChannel, currentTeam} = this.props; if (term) { @@ -264,15 +241,22 @@ class ChannelAddMembers extends PureComponent { style={{marginVertical: 5}} > diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js index 02f720e1c..135a99dc9 100644 --- a/app/screens/channel_info/channel_info.js +++ b/app/screens/channel_info/channel_info.js @@ -16,6 +16,7 @@ import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import {General} from 'mattermost-redux/constants'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; import ChannelInfoHeader from './channel_info_header'; import ChannelInfoRow from './channel_info_row'; @@ -63,6 +64,11 @@ class ChannelInfo extends PureComponent { } } + close = () => { + EventEmitter.emit(General.DEFAULT_CHANNEL, ''); + this.props.navigator.pop({animated: true}); + }; + goToChannelAddMembers = () => { const {intl, navigator, theme} = this.props; navigator.push({ @@ -115,7 +121,7 @@ class ChannelInfo extends PureComponent { }; onPressAction = () => { this.props.actions.leaveChannel(channel, true).then(() => { - this.props.navigator.pop({animated: true}); + this.close(); }); }; } else if (eventType === 'delete') { @@ -126,7 +132,7 @@ class ChannelInfo extends PureComponent { }; onPressAction = () => { this.props.actions.deleteChannel(channel.id).then(() => { - this.props.navigator.pop({animated: true}); + this.close(); }); }; } @@ -157,12 +163,12 @@ class ChannelInfo extends PureComponent { switch (channel.type) { case General.DM_CHANNEL: closeDMChannel(channel).then(() => { - this.props.navigator.pop({animated: true}); + this.close(); }); break; case General.GM_CHANNEL: closeGMChannel(channel).then(() => { - this.props.navigator.pop({animated: true}); + this.close(); }); break; } diff --git a/app/screens/channel_members/channel_members.js b/app/screens/channel_members/channel_members.js index f506b983e..7574e72a7 100644 --- a/app/screens/channel_members/channel_members.js +++ b/app/screens/channel_members/channel_members.js @@ -5,7 +5,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { Alert, - Keyboard, Platform, InteractionManager, StyleSheet, @@ -76,10 +75,6 @@ class ChannelMembers extends PureComponent { } componentDidMount() { - if (Platform.OS === 'android') { - Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); - } - InteractionManager.runAfterInteractions(() => { this.props.actions.getProfilesInChannel(this.props.currentChannel.id, 0); }); @@ -87,12 +82,6 @@ class ChannelMembers extends PureComponent { this.emitCanRemoveMembers(false); } - componentWillUnmount() { - if (Platform.OS === 'android') { - Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); - } - } - componentWillReceiveProps(nextProps) { const {requestStatus} = this.props; if (requestStatus === RequestStatus.STARTED && @@ -159,10 +148,6 @@ class ChannelMembers extends PureComponent { }); }; - handleAndroidKeyboard = () => { - this.onSearchButtonPress(); - }; - handleRemoveMembersPress = () => { const {selectedMembers} = this.state; const membersToRemove = Object.keys(selectedMembers).filter((m) => selectedMembers[m]); @@ -240,10 +225,6 @@ class ChannelMembers extends PureComponent { } }; - onSearchButtonPress = () => { - this.searchBar.blur(); - }; - removeMembers = (membersToRemove) => { const {actions, currentChannel} = this.props; actions.handleRemoveChannelMembers(currentChannel.id, membersToRemove); @@ -275,12 +256,8 @@ class ChannelMembers extends PureComponent { ); }; - searchBarRef = (ref) => { - this.searchBar = ref; - }; - - searchProfiles = (event) => { - const term = event.nativeEvent.text.toLowerCase(); + searchProfiles = (text) => { + const term = text.toLowerCase(); if (term) { this.setState({searching: true, term}); @@ -319,15 +296,22 @@ class ChannelMembers extends PureComponent { style={{marginVertical: 5}} > diff --git a/app/screens/more_channels/more_channels.js b/app/screens/more_channels/more_channels.js index b21348526..a897a7cd9 100644 --- a/app/screens/more_channels/more_channels.js +++ b/app/screens/more_channels/more_channels.js @@ -5,7 +5,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {injectIntl, intlShape} from 'react-intl'; import { - Keyboard, Platform, InteractionManager, StyleSheet, @@ -79,10 +78,6 @@ class MoreChannels extends PureComponent { } componentDidMount() { - if (Platform.OS === 'android') { - Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); - } - // set the timeout to 400 cause is the time that the modal takes to open // Somehow interactionManager doesn't care setTimeout(() => { @@ -104,12 +99,6 @@ class MoreChannels extends PureComponent { } } - componentWillUnmount() { - if (Platform.OS === 'android') { - Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); - } - } - close = () => { this.props.navigator.dismissModal({animationType: 'slide-down'}); }; @@ -132,20 +121,8 @@ class MoreChannels extends PureComponent { }); }; - handleAndroidKeyboard = () => { - this.onSearchButtonPress(); - }; - - searchBarRef = (ref) => { - this.searchBar = ref; - }; - - onSearchButtonPress = () => { - this.searchBar.blur(); - }; - - searchProfiles = (event) => { - const term = event.nativeEvent.text.toLowerCase(); + searchProfiles = (text) => { + const term = text.toLowerCase(); if (term) { const channels = this.filterChannels(this.state.channels, term); @@ -226,7 +203,6 @@ class MoreChannels extends PureComponent { onSelectChannel = async (id) => { this.emitCanCreateChannel(false); this.setState({adding: true}); - this.searchBar.blur(); await this.props.actions.joinChannel( this.props.currentUserId, this.props.currentTeamId, @@ -278,15 +254,22 @@ class MoreChannels extends PureComponent { style={{marginVertical: 5}} > diff --git a/app/screens/more_dms/more_dms.js b/app/screens/more_dms/more_dms.js index 6079edd29..1d51500a8 100644 --- a/app/screens/more_dms/more_dms.js +++ b/app/screens/more_dms/more_dms.js @@ -5,7 +5,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {injectIntl, intlShape} from 'react-intl'; import { - Keyboard, Platform, InteractionManager, StyleSheet, @@ -70,10 +69,6 @@ class MoreDirectMessages extends PureComponent { } componentDidMount() { - if (Platform.OS === 'android') { - Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); - } - // set the timeout to 400 cause is the time that the modal takes to open // Somehow interactionManager doesn't care setTimeout(() => { @@ -81,26 +76,12 @@ class MoreDirectMessages extends PureComponent { }, 400); } - componentWillUnmount() { - if (Platform.OS === 'android') { - Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); - } - } - close = () => { this.props.navigator.dismissModal({ animationType: 'slide-down' }); }; - handleAndroidKeyboard = () => { - this.onSearchButtonPress(); - }; - - searchBarRef = (ref) => { - this.searchBar = ref; - }; - onNavigatorEvent = (event) => { if (event.type === 'NavBarButtonPress') { if (event.id === 'close-dms') { @@ -109,15 +90,11 @@ class MoreDirectMessages extends PureComponent { } }; - onSearchButtonPress = () => { - this.searchBar.blur(); - }; - - searchProfiles = (event) => { - const term = event.nativeEvent.text; + searchProfiles = (text) => { + const term = text.toLowerCase(); if (term) { - this.setState({searching: true, term: term.toLowerCase()}); + this.setState({searching: true, term}); clearTimeout(this.searchTimeoutId); this.searchTimeoutId = setTimeout(() => { @@ -156,7 +133,6 @@ class MoreDirectMessages extends PureComponent { onSelectMember = async (id) => { this.setState({adding: true}); - this.searchBar.blur(); await this.props.actions.makeDirectChannel(id); EventEmitter.emit('close_channel_drawer'); @@ -190,15 +166,22 @@ class MoreDirectMessages extends PureComponent { style={{marginVertical: 5}} > diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 963ca1dc0..8c7813a84 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -1684,6 +1684,7 @@ "mobile.account_notifications.threads_mentions": "Mentions in threads", "mobile.account_notifications.threads_start": "Threads that I start", "mobile.account_notifications.threads_start_participate": "Threads that I start or participate in", + "mobile.channel_drawer.search": "Jump to a conversation", "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}?", "mobile.channel_info.alertNo": "No", @@ -1700,6 +1701,7 @@ "mobile.channel_list.closeGM": "Close Group Message", "mobile.channel_list.dm": "Direct Message", "mobile.channel_list.gm": "Group Message", + "mobile.channel_list.not_member": "NOT A MEMBER", "mobile.channel_list.open": "Open {term}", "mobile.channel_list.openDM": "Open Direct Message", "mobile.channel_list.openGM": "Open Group Message", @@ -1716,6 +1718,7 @@ "mobile.create_channel.private": "New Private Channel", "mobile.create_channel.public": "New Public Channel", "mobile.custom_list.no_results": "No Results", + "mobile.drawer.teamsTitle": "Teams", "mobile.edit_post.title": "Editing Message", "mobile.file_upload.camera": "Take Photo or Video", "mobile.file_upload.library": "Photo Library", diff --git a/package.json b/package.json index 9ff2ba364..1e0c1e5c1 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "react-native-notifications": "enahum/react-native-notifications.git", "react-native-orientation": "enahum/react-native-orientation.git", "react-native-search-bar": "enahum/react-native-search-bar.git", + "react-native-search-box": "0.0.9", "react-native-svg": "5.1.8", "react-native-swiper": "1.5.4", "react-native-tooltip": "enahum/react-native-tooltip", diff --git a/yarn.lock b/yarn.lock index f417aa29d..3d0aa2e23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3645,7 +3645,7 @@ makeerror@1.0.x: mattermost-redux@mattermost/mattermost-redux#master: version "0.0.1" - resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/9a2e8099daea436dab76aa612be60492311387b3" + resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/488faa22c03e1c187c714a5db7f0a42b69d50ae8" dependencies: deep-equal "1.0.1" harmony-reflect "1.5.1" @@ -4556,6 +4556,10 @@ react-native-search-bar@enahum/react-native-search-bar.git: version "2.16.0" resolved "https://codeload.github.com/enahum/react-native-search-bar/tar.gz/88e6f5ba68012440ec21bde8388ca9e3124921c0" +react-native-search-box@0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/react-native-search-box/-/react-native-search-box-0.0.9.tgz#c1677e127d6d2b346850b031b2cb216447fa66f0" + react-native-svg-mock@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/react-native-svg-mock/-/react-native-svg-mock-1.0.2.tgz#2dc35fb97ca1e0ea393ece6d23782ba4f58e178f" @@ -4575,10 +4579,6 @@ react-native-tooltip@enahum/react-native-tooltip: version "5.0.0" resolved "https://codeload.github.com/enahum/react-native-tooltip/tar.gz/97d58d19636df8d8df66d6b5737154c9fab727c8" -react-native-tooltip@enahum/react-native-tooltip.git: - version "5.0.0" - resolved "https://codeload.github.com/enahum/react-native-tooltip/tar.gz/d4b491c7ef1f93cc6c62e5012e36be1087b45b34" - react-native-vector-icons@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-4.1.1.tgz#9ac75bde77d9243346668c51dca7756775428087"