[ICU-629] Add support for DM with deactivated channel (teammate) (#1385)

* Add support for DM with deactivated channel (teammate)

* add archive_icon

* add extra protection to prevent error when teammate is evetually deactivated
This commit is contained in:
Saturnino Abril 2018-02-01 00:04:31 +08:00 committed by GitHub
parent cd80cdfdf4
commit bda1ba153e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 153 additions and 32 deletions

View file

@ -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}
/>

View file

@ -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
};

View file

@ -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 {
</Text>
</View>
);
} else if (type === General.DM_CHANNEL && teammateDeletedAt) {
icon = (
<ArchiveIcon
width={size}
height={size}
color={offlineColor}
/>
);
} else if (type === General.DM_CHANNEL) {
switch (status) {
case General.AWAY:

View file

@ -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;

View file

@ -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,

View file

@ -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 (
<Text style={style.deactivatedMessage}>
{intl.formatMessage({
id: 'create_post.deactivated',
defaultMessage: 'You are viewing an archived channel with a deactivated user.'
})}
</Text>
);
}
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,

View file

@ -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 (
<View style={{height, width, alignItems: 'flex-start'}}>
<Svg
width={width}
height={height}
viewBox='0 0 14 14'
>
<Path
d='M8.5 6.5q0-0.203-0.148-0.352t-0.352-0.148h-2q-0.203 0-0.352 0.148t-0.148 0.352 0.148 0.352 0.352 0.148h2q0.203 0 0.352-0.148t0.148-0.352zM13 5v7.5q0 0.203-0.148 0.352t-0.352 0.148h-11q-0.203 0-0.352-0.148t-0.148-0.352v-7.5q0-0.203 0.148-0.352t0.352-0.148h11q0.203 0 0.352 0.148t0.148 0.352zM13.5 1.5v2q0 0.203-0.148 0.352t-0.352 0.148h-12q-0.203 0-0.352-0.148t-0.148-0.352v-2q0-0.203 0.148-0.352t0.352-0.148h12q0.203 0 0.352 0.148t0.148 0.352z'
fill={color}
/>
</Svg>
</View>
);
}
}

View file

@ -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,

29
app/selectors/channel.js Normal file
View file

@ -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];
}
);