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 079e3cad5..ab30d49b4 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 @@ -31,6 +31,7 @@ export default class ChannelItem extends PureComponent { navigator: PropTypes.object, onSelectChannel: PropTypes.func.isRequired, status: PropTypes.string, + teammateDeletedAt: PropTypes.number, type: PropTypes.string.isRequired, theme: PropTypes.object.isRequired }; @@ -79,6 +80,7 @@ export default class ChannelItem extends PureComponent { isUnread, mentions, status, + teammateDeletedAt, theme, type } = this.props; @@ -133,6 +135,7 @@ export default class ChannelItem extends PureComponent { membersCount={displayName.split(',').length} size={16} status={status} + teammateDeletedAt={teammateDeletedAt} theme={theme} type={type} /> diff --git a/app/components/channel_drawer/channels_list/channel_item/index.js b/app/components/channel_drawer/channels_list/channel_item/index.js index 41ff2b4e6..9515a41f9 100644 --- a/app/components/channel_drawer/channels_list/channel_item/index.js +++ b/app/components/channel_drawer/channels_list/channel_item/index.js @@ -6,7 +6,7 @@ import {connect} from 'react-redux'; import {General} from 'mattermost-redux/constants'; import {getCurrentChannelId, makeGetChannel, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; import ChannelItem from './channel_item'; @@ -22,8 +22,13 @@ function makeMapStateToProps() { const currentUserId = getCurrentUserId(state); let isMyUser = false; + let teammateDeletedAt = 0; if (channel.type === General.DM_CHANNEL && channel.teammate_id) { isMyUser = channel.teammate_id === currentUserId; + const teammate = getUser(state, channel.teammate_id); + if (teammate && teammate.delete_at) { + teammateDeletedAt = teammate.delete_at; + } } return { @@ -33,6 +38,7 @@ function makeMapStateToProps() { isMyUser, mentions: member ? member.mention_count : 0, status: channel.status, + teammateDeletedAt, theme: getTheme(state), type: channel.type }; diff --git a/app/components/channel_icon.js b/app/components/channel_icon.js index edc78b16d..ff4585d7f 100644 --- a/app/components/channel_icon.js +++ b/app/components/channel_icon.js @@ -9,7 +9,13 @@ import { } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; -import {AwayAvatar, DndAvatar, OfflineAvatar, OnlineAvatar} from 'app/components/status_icons'; +import { + ArchiveIcon, + AwayAvatar, + DndAvatar, + OfflineAvatar, + OnlineAvatar +} from 'app/components/status_icons'; import {General} from 'mattermost-redux/constants'; @@ -23,6 +29,7 @@ export default class ChannelIcon extends React.PureComponent { membersCount: PropTypes.number, size: PropTypes.number, status: PropTypes.string, + teammateDeletedAt: PropTypes.number, theme: PropTypes.object.isRequired, type: PropTypes.string.isRequired }; @@ -35,7 +42,17 @@ export default class ChannelIcon extends React.PureComponent { }; render() { - const {isActive, isUnread, isInfo, membersCount, size, status, theme, type} = this.props; + const { + isActive, + isUnread, + isInfo, + membersCount, + size, + status, + teammateDeletedAt, + theme, + type + } = this.props; const style = getStyleSheet(theme); let activeIcon; @@ -89,6 +106,14 @@ export default class ChannelIcon extends React.PureComponent { ); + } else if (type === General.DM_CHANNEL && teammateDeletedAt) { + icon = ( + + ); } else if (type === General.DM_CHANNEL) { switch (status) { case General.AWAY: diff --git a/app/components/channel_intro/index.js b/app/components/channel_intro/index.js index 51900a86c..496af524b 100644 --- a/app/components/channel_intro/index.js +++ b/app/components/channel_intro/index.js @@ -10,24 +10,14 @@ import {getCurrentUserId, getUser, makeGetProfilesInChannel} from 'mattermost-re import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getChannelMembersForDm} from 'app/selectors/channel'; + import ChannelIntro from './channel_intro'; function makeMapStateToProps() { const getChannel = makeGetChannel(); const getProfilesInChannel = makeGetProfilesInChannel(); - const getOtherUserIdForDm = createSelector( - (state, channel) => channel, - getCurrentUserId, - (channel, currentUserId) => { - if (!channel) { - return ''; - } - - return channel.name.split('__').find((m) => m !== currentUserId) || currentUserId; - } - ); - const getChannelMembers = createSelector( getCurrentUserId, (state, channel) => getProfilesInChannel(state, channel.id), @@ -37,17 +27,6 @@ function makeMapStateToProps() { } ); - const getChannelMembersForDm = createSelector( - (state, channel) => getUser(state, getOtherUserIdForDm(state, channel)), - (otherUser) => { - if (!otherUser) { - return []; - } - - return [otherUser]; - } - ); - return function mapStateToProps(state, ownProps) { const currentChannel = getChannel(state, {id: ownProps.channelId}) || {}; const {status: getPostsRequestStatus} = state.requests.posts.getPosts; diff --git a/app/components/post_textbox/index.js b/app/components/post_textbox/index.js index 37f538472..02a0fe526 100644 --- a/app/components/post_textbox/index.js +++ b/app/components/post_textbox/index.js @@ -4,8 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; +import {General} from 'mattermost-redux/constants'; + import {createPost} from 'mattermost-redux/actions/posts'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; import {canUploadFilesOnMobile} from 'mattermost-redux/selectors/entities/general'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; @@ -17,17 +19,28 @@ import {handleClearFiles, handleClearFailedFiles, handleRemoveLastFile, handleUp import {handleCommentDraftChanged, handleCommentDraftSelectionChanged} from 'app/actions/views/thread'; import {userTyping} from 'app/actions/views/typing'; import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views'; +import {getChannelMembersForDm} from 'app/selectors/channel'; import PostTextbox from './post_textbox'; function mapStateToProps(state, ownProps) { const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state); + const currentChannel = getCurrentChannel(state); + let deactivatedChannel = false; + if (currentChannel.type === General.DM_CHANNEL) { + const teammate = getChannelMembersForDm(state, currentChannel); + if (teammate.length && teammate[0].delete_at) { + deactivatedChannel = true; + } + } + return { - channelId: ownProps.channelId || getCurrentChannelId(state), + channelId: ownProps.channelId || currentChannel.id, canUploadFiles: canUploadFilesOnMobile(state), channelIsLoading: state.views.channel.loading, currentUserId: getCurrentUserId(state), + deactivatedChannel, files: currentDraft.files, theme: getTheme(state), uploadFileRequestStatus: state.requests.files.uploadFiles.status, diff --git a/app/components/post_textbox/post_textbox.js b/app/components/post_textbox/post_textbox.js index 736b0f588..23ff73016 100644 --- a/app/components/post_textbox/post_textbox.js +++ b/app/components/post_textbox/post_textbox.js @@ -3,7 +3,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Alert, BackHandler, Keyboard, Platform, TextInput, TouchableOpacity, View} from 'react-native'; +import {Alert, BackHandler, Keyboard, Platform, Text, TextInput, TouchableOpacity, View} from 'react-native'; import {intlShape} from 'react-intl'; import {RequestStatus} from 'mattermost-redux/constants'; @@ -41,6 +41,7 @@ export default class PostTextbox extends PureComponent { channelId: PropTypes.string.isRequired, channelIsLoading: PropTypes.bool.isRequired, currentUserId: PropTypes.string.isRequired, + deactivatedChannel: PropTypes.bool.isRequired, files: PropTypes.array, navigator: PropTypes.object, rootId: PropTypes.string, @@ -95,7 +96,9 @@ export default class PostTextbox extends PureComponent { }; blur = () => { - this.refs.input.blur(); + if (this.refs.input) { + this.refs.input.blur(); + } }; canSend = () => { @@ -409,16 +412,28 @@ export default class PostTextbox extends PureComponent { canUploadFiles, channelId, channelIsLoading, + deactivatedChannel, files, navigator, rootId, theme } = this.props; - const {showFileMaxWarning} = this.state; const style = getStyleSheet(theme); - const textInputHeight = Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT); + if (deactivatedChannel) { + return ( + + {intl.formatMessage({ + id: 'create_post.deactivated', + defaultMessage: 'You are viewing an archived channel with a deactivated user.' + })} + + ); + } + const {showFileMaxWarning} = this.state; + + const textInputHeight = Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT); const textValue = channelIsLoading ? '' : this.state.value; let placeholder; @@ -533,6 +548,19 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { borderTopWidth: 1, borderTopColor: changeOpacity(theme.centerChannelColor, 0.20) }, + deactivatedMessage: { + color: changeOpacity(theme.centerChannelColor, 0.8), + fontSize: 15, + lineHeight: 22, + alignItems: 'flex-end', + flexDirection: 'row', + paddingVertical: 4, + backgroundColor: theme.centerChannelBg, + borderTopWidth: 1, + borderTopColor: changeOpacity(theme.centerChannelColor, 0.20), + marginLeft: 10, + marginRight: 10 + }, sendButtonContainer: { justifyContent: 'flex-end', paddingHorizontal: 5, diff --git a/app/components/status_icons/archive_icon.js b/app/components/status_icons/archive_icon.js new file mode 100644 index 000000000..126102b4a --- /dev/null +++ b/app/components/status_icons/archive_icon.js @@ -0,0 +1,36 @@ +// Copyright (c) 2018-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 Svg, { + Path +} from 'react-native-svg'; + +export default class ArchiveIcon extends PureComponent { + static propTypes = { + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + color: PropTypes.string.isRequired + }; + + render() { + const {color, height, width} = this.props; + + return ( + + + + + + ); + } +} diff --git a/app/components/status_icons/index.js b/app/components/status_icons/index.js index c989b3f1e..615c5983a 100644 --- a/app/components/status_icons/index.js +++ b/app/components/status_icons/index.js @@ -1,6 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import ArchiveIcon from './archive_icon'; import AwayAvatar from './away_avatar'; import AwayIcon from './away_icon'; import DndAvatar from './dnd_avatar'; @@ -11,6 +12,7 @@ import OnlineAvatar from './online_avatar'; import OnlineIcon from './online_icon'; export { + ArchiveIcon, AwayAvatar, AwayIcon, DndAvatar, diff --git a/app/selectors/channel.js b/app/selectors/channel.js new file mode 100644 index 000000000..eef0a06d4 --- /dev/null +++ b/app/selectors/channel.js @@ -0,0 +1,29 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {createSelector} from 'reselect'; + +import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; + +const getOtherUserIdForDm = createSelector( + (state, channel) => channel, + getCurrentUserId, + (channel, currentUserId) => { + if (!channel) { + return ''; + } + + return channel.name.split('__').find((m) => m !== currentUserId) || currentUserId; + } +); + +export const getChannelMembersForDm = createSelector( + (state, channel) => getUser(state, getOtherUserIdForDm(state, channel)), + (otherUser) => { + if (!otherUser) { + return []; + } + + return [otherUser]; + } +);