Add ability to post messages (#158)
* Fixed copyright date * Removed unnecessary components folder from channel scene * Added initial version of post textbox * Moved getTheme into a proper selector * Moved ChannelHeader into its own component * Flipped post list so that the most recent posts appear at the bottom * Moved post textbox value into redux view store * Switched new components from PureRenderMixin to React.PureComponent
This commit is contained in:
parent
fdc9b5cc0e
commit
d0481605fb
27 changed files with 548 additions and 125 deletions
|
|
@ -1,13 +1,16 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {fetchMyChannelsAndMembers, getMyChannelMembers, selectChannel} from 'service/actions/channels';
|
||||
import {getPosts} from 'service/actions/posts';
|
||||
import {getTeamMembersByIds} from 'service/actions/teams';
|
||||
import {Constants, UsersTypes} from 'service/constants';
|
||||
import {getChannelByName, getDirectChannelName} from 'service/utils/channel_utils';
|
||||
import {getPreferencesByCategory} from 'service/utils/preference_utils';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
export function loadChannelsIfNecessary(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
|
|
@ -101,3 +104,12 @@ export function selectInitialChannel(teamId) {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePostDraftChanged(postDraft) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ViewTypes.POST_DRAFT_CHANGED,
|
||||
postDraft
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
import Badge from './badge';
|
||||
import {TouchableHighlight, Text, View} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {OnlineStatus, AwayStatus, OfflineStatus} from 'app/components/status_icons';
|
||||
import {changeOpacity} from 'app/utils/colors';
|
||||
|
||||
import {Constants} from 'service/constants';
|
||||
|
||||
import Badge from './badge';
|
||||
|
||||
export default class ChannelItem extends React.Component {
|
||||
static propTypes = {
|
||||
channel: React.PropTypes.object.isRequired,
|
||||
|
|
@ -18,28 +22,6 @@ export default class ChannelItem extends React.Component {
|
|||
theme: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
changeOpacity = (oldColor, opacity) => {
|
||||
let color = oldColor;
|
||||
if (color[0] === '#') {
|
||||
color = color.slice(1);
|
||||
}
|
||||
|
||||
if (color.length === 3) {
|
||||
const tempColor = color;
|
||||
color = '';
|
||||
|
||||
color += tempColor[0] + tempColor[0];
|
||||
color += tempColor[1] + tempColor[1];
|
||||
color += tempColor[2] + tempColor[2];
|
||||
}
|
||||
|
||||
const r = parseInt(color.substring(0, 2), 16);
|
||||
const g = parseInt(color.substring(2, 4), 16);
|
||||
const b = parseInt(color.substring(4, 6), 16);
|
||||
|
||||
return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
channel,
|
||||
|
|
@ -49,7 +31,7 @@ export default class ChannelItem extends React.Component {
|
|||
isActive
|
||||
} = this.props;
|
||||
|
||||
let iconColor = this.changeOpacity(theme.centerChannelColor, 0.7);
|
||||
let iconColor = changeOpacity(theme.centerChannelColor, 0.7);
|
||||
let icon;
|
||||
let activeBorder;
|
||||
let badge;
|
||||
|
|
@ -112,7 +94,7 @@ export default class ChannelItem extends React.Component {
|
|||
iconColor = theme.sidebarTextActiveColor;
|
||||
style.color = theme.sidebarTextActiveColor;
|
||||
style.opacity = 1;
|
||||
itemStyle.backgroundColor = this.changeOpacity(theme.sidebarTextActiveColor, 0.1);
|
||||
itemStyle.backgroundColor = changeOpacity(theme.sidebarTextActiveColor, 0.1);
|
||||
|
||||
activeBorder = (
|
||||
<View style={activeStyle}/>
|
||||
|
|
@ -160,7 +142,7 @@ export default class ChannelItem extends React.Component {
|
|||
<OfflineStatus
|
||||
width={13}
|
||||
height={13}
|
||||
color={this.changeOpacity(theme.centerChannelColor, 0.7)}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.7)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
|
@ -169,7 +151,7 @@ export default class ChannelItem extends React.Component {
|
|||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
underlayColor={this.changeOpacity(theme.sidebarTextHoverBg, 0.3)}
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.3)}
|
||||
onPress={() => this.props.onSelectChannel(channel)}
|
||||
>
|
||||
<View style={{flex: 1}}>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {isSystemMessage} from 'service/utils/post_utils.js';
|
|||
|
||||
export default class Post extends React.Component {
|
||||
static propTypes = {
|
||||
style: React.PropTypes.object,
|
||||
post: React.PropTypes.object.isRequired,
|
||||
user: React.PropTypes.object,
|
||||
theme: React.PropTypes.object.isRequired
|
||||
|
|
@ -32,7 +33,7 @@ export default class Post extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={this.props.style}>
|
||||
<Text>
|
||||
{'['}
|
||||
<FormattedTime value={this.props.post.create_at}/>
|
||||
|
|
|
|||
|
|
@ -28,12 +28,18 @@ export default class PostList extends React.Component {
|
|||
}
|
||||
|
||||
renderPost(post) {
|
||||
return <Post post={post}/>;
|
||||
return (
|
||||
<Post
|
||||
style={{transform: [{rotate: '180deg'}]}}
|
||||
post={post}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ListView
|
||||
style={{transform: [{rotate: '180deg'}]}}
|
||||
enableEmptySections={true}
|
||||
dataSource={this.state.dataSource}
|
||||
renderRow={this.renderPost}
|
||||
|
|
|
|||
40
app/components/post_textbox/components/paper_clip_icon.js
Normal file
40
app/components/post_textbox/components/paper_clip_icon.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import Svg, {G, Path} from 'react-native-svg';
|
||||
|
||||
export default class PaperClipIcon extends React.Component {
|
||||
static propTypes = {
|
||||
width: React.PropTypes.number.isRequired,
|
||||
height: React.PropTypes.number.isRequired,
|
||||
color: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
setNativeProps = (nativeProps) => {
|
||||
if (this.root) {
|
||||
this.root.setNativeProps(nativeProps);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Svg
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
x='0'
|
||||
y='0'
|
||||
viewBox='0 0 48 48'
|
||||
enableBackground='new 0 0 48 48'
|
||||
space='preserve'
|
||||
>
|
||||
<G>
|
||||
<Path
|
||||
d='M43.922,6.653c-2.643-2.644-6.201-4.107-9.959-4.069c-3.774,0.019-7.32,1.497-9.983,4.161l-12.3,12.3l-8.523,8.521 c-4.143,4.144-4.217,10.812-0.167,14.862c1.996,1.996,4.626,2.989,7.277,2.989c2.73,0,5.482-1.055,7.583-3.156l15.547-15.545 c0.002-0.002,0.002-0.004,0.004-0.005l5.358-5.358c1.394-1.393,2.176-3.24,2.201-5.2c0.026-1.975-0.716-3.818-2.09-5.192 c-2.834-2.835-7.496-2.787-10.394,0.108L9.689,29.857c-0.563,0.563-0.563,1.474,0,2.036c0.281,0.28,0.649,0.421,1.018,0.421 c0.369,0,0.737-0.141,1.018-0.421l18.787-18.788c1.773-1.774,4.609-1.824,6.322-0.11c0.82,0.82,1.263,1.928,1.247,3.119 c-0.017,1.205-0.497,2.342-1.357,3.201l-5.55,5.551c-0.002,0.002-0.002,0.004-0.004,0.005L15.814,40.225 c-3.02,3.02-7.86,3.094-10.789,0.167c-2.928-2.929-2.854-7.77,0.167-10.791l0.958-0.958c0.001-0.002,0.004-0.002,0.005-0.004 L26.016,8.78c2.123-2.124,4.951-3.303,7.961-3.317c2.998,0.02,5.814,1.13,7.91,3.226c4.35,4.351,4.309,11.472-0.093,15.873 L25.459,40.895c-0.563,0.562-0.563,1.473,0,2.035c0.281,0.281,0.65,0.422,1.018,0.422c0.369,0,0.737-0.141,1.018-0.422 L43.83,26.596C49.354,21.073,49.395,12.126,43.922,6.653z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
6
app/components/post_textbox/index.js
Normal file
6
app/components/post_textbox/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostTextboxContainer from './post_textbox_container';
|
||||
|
||||
export default PostTextboxContainer;
|
||||
145
app/components/post_textbox/post_textbox.js
Normal file
145
app/components/post_textbox/post_textbox.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {TouchableHighlight, View} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
|
||||
import {changeOpacity} from 'app/utils/colors';
|
||||
|
||||
// import PaperClipIcon from './components/paper_clip_icon.js';
|
||||
|
||||
const MAX_CONTENT_HEIGHT = 100;
|
||||
|
||||
export default class PostTextbox extends React.PureComponent {
|
||||
static propTypes = {
|
||||
currentUserId: React.PropTypes.string.isRequired,
|
||||
teamId: React.PropTypes.string.isRequired,
|
||||
channelId: React.PropTypes.string.isRequired,
|
||||
rootId: React.PropTypes.string,
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChangeText: React.PropTypes.func.isRequired,
|
||||
theme: React.PropTypes.object.isRequired,
|
||||
actions: React.PropTypes.shape({
|
||||
createPost: React.PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
rootId: ''
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
contentHeight: 0
|
||||
};
|
||||
}
|
||||
|
||||
blur = () => {
|
||||
this.refs.input.getWrappedInstance().blur();
|
||||
}
|
||||
|
||||
handleContentSizeChange = (e) => {
|
||||
this.setState({
|
||||
contentHeight: e.nativeEvent.contentSize.height
|
||||
});
|
||||
}
|
||||
|
||||
sendMessage = () => {
|
||||
if (this.props.value.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const post = {
|
||||
user_id: this.props.currentUserId,
|
||||
channel_id: this.props.channelId,
|
||||
root_id: this.props.rootId,
|
||||
parent_id: this.props.rootId,
|
||||
message: this.props.value
|
||||
};
|
||||
|
||||
this.props.actions.createPost(this.props.teamId, post);
|
||||
this.props.onChangeText('');
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = this.props.theme;
|
||||
|
||||
let placeholder;
|
||||
if (this.props.rootId) {
|
||||
placeholder = {id: 'create_comment.addComment', defaultMessage: 'Add a comment...'};
|
||||
} else {
|
||||
placeholder = {id: 'create_post.write', defaultMessage: 'Write a message...'};
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
alignItems: 'flex-end',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
padding: 7
|
||||
}}
|
||||
>
|
||||
{/*<TouchableHighlight
|
||||
style={{
|
||||
height: 36,
|
||||
padding: 9,
|
||||
width: 36
|
||||
}}
|
||||
>
|
||||
<PaperClipIcon
|
||||
width={18}
|
||||
height={18}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.9)}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
<View style={{width: 7}}/>*/}
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
ref='input'
|
||||
value={this.props.value}
|
||||
onChangeText={this.props.onChangeText}
|
||||
onContentSizeChange={this.handleContentSizeChange}
|
||||
placeholder={placeholder}
|
||||
onSubmitEditing={this.sendMessage}
|
||||
multiline={true}
|
||||
style={{
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderWidth: 1,
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
height: Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT),
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
paddingTop: 6
|
||||
}}
|
||||
/>
|
||||
<View style={{width: 7}}/>
|
||||
<TouchableHighlight
|
||||
onPress={this.sendMessage}
|
||||
style={{
|
||||
height: 36,
|
||||
width: 36
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name='paper-plane'
|
||||
size={18}
|
||||
style={{
|
||||
color: theme.linkColor,
|
||||
padding: 9
|
||||
}}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
29
app/components/post_textbox/post_textbox_container.js
Normal file
29
app/components/post_textbox/post_textbox_container.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {createPost} from 'service/actions/posts';
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'service/selectors/entities/users';
|
||||
|
||||
import PostTextbox from './post_textbox';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
createPost
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(PostTextbox);
|
||||
32
app/components/text_input_with_localized_placeholder.js
Normal file
32
app/components/text_input_with_localized_placeholder.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {TextInput} from 'react-native';
|
||||
|
||||
class TextInputWithLocalizedPlaceholder extends React.PureComponent {
|
||||
static propTypes = {
|
||||
...TextInput.propTypes,
|
||||
placeholder: React.PropTypes.object.isRequired,
|
||||
intl: intlShape.isRequired
|
||||
};
|
||||
|
||||
blur = () => {
|
||||
this.refs.input.blur();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {intl, placeholder, ...otherProps} = this.props;
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
ref='input'
|
||||
{...otherProps}
|
||||
placeholder={intl.formatMessage(placeholder)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(TextInputWithLocalizedPlaceholder, {withRef: true});
|
||||
|
|
@ -9,7 +9,8 @@ const ViewTypes = keyMirror({
|
|||
LOGIN_ID_CHANGED: null,
|
||||
PASSWORD_CHANGED: null,
|
||||
|
||||
TOGGLE_CHANNEL_DRAWER: null
|
||||
TOGGLE_CHANNEL_DRAWER: null,
|
||||
POST_DRAFT_CHANGED: null
|
||||
});
|
||||
|
||||
export default ViewTypes;
|
||||
|
|
|
|||
23
app/reducers/views/channel.js
Normal file
23
app/reducers/views/channel.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {ChannelTypes} from 'service/constants';
|
||||
|
||||
function postDraft(state = '', action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.POST_DRAFT_CHANGED:
|
||||
return action.postDraft;
|
||||
case ChannelTypes.SELECT_CHANNEL:
|
||||
return '';
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
postDraft
|
||||
});
|
||||
|
|
@ -3,14 +3,16 @@
|
|||
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import channel from './channel';
|
||||
import drawer from './drawer';
|
||||
import i18n from './i18n';
|
||||
import login from './login';
|
||||
import selectServer from './select_server';
|
||||
import i18n from './i18n';
|
||||
import drawer from './drawer';
|
||||
|
||||
export default combineReducers({
|
||||
channel,
|
||||
drawer,
|
||||
i18n,
|
||||
login,
|
||||
selectServer,
|
||||
drawer
|
||||
selectServer
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,34 +2,38 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import {StatusBar, Text, TouchableHighlight, View} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
StatusBar,
|
||||
Text
|
||||
} from 'react-native';
|
||||
import Drawer from 'react-native-drawer';
|
||||
|
||||
import ChannelDrawer from 'app/components/channel_drawer';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
import RightSidebarMenu from 'app/components/right_sidebar_menu';
|
||||
|
||||
import ChannelPostList from './components/channel_post_list';
|
||||
import ChannelHeader from './channel_header';
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
||||
export default class Channel extends React.Component {
|
||||
export default class Channel extends React.PureComponent {
|
||||
static propTypes = {
|
||||
actions: React.PropTypes.shape({
|
||||
loadChannelsIfNecessary: React.PropTypes.func.isRequired,
|
||||
loadProfilesAndTeamMembersForDMSidebar: React.PropTypes.func.isRequired,
|
||||
selectInitialChannel: React.PropTypes.func.isRequired,
|
||||
openChannelDrawer: React.PropTypes.func.isRequired
|
||||
openChannelDrawer: React.PropTypes.func.isRequired,
|
||||
handlePostDraftChanged: React.PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
currentTeam: React.PropTypes.object,
|
||||
currentChannel: React.PropTypes.object,
|
||||
postDraft: React.PropTypes.string.isRequired,
|
||||
theme: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
this.state = {
|
||||
leftSidebarOpen: false,
|
||||
rightSidebarOpen: false
|
||||
|
|
@ -56,6 +60,7 @@ export default class Channel extends React.Component {
|
|||
};
|
||||
|
||||
openRightSidebar = () => {
|
||||
this.refs.postTextbox.getWrappedInstance().blur();
|
||||
this.setState({rightSidebarOpen: true});
|
||||
};
|
||||
|
||||
|
|
@ -63,6 +68,11 @@ export default class Channel extends React.Component {
|
|||
this.setState({rightSidebarOpen: false});
|
||||
};
|
||||
|
||||
openChannelDrawer = () => {
|
||||
this.refs.postTextbox.getWrappedInstance().blur();
|
||||
this.props.actions.openChannelDrawer();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentTeam,
|
||||
|
|
@ -77,7 +87,10 @@ export default class Channel extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, backgroundColor: theme.centerChannelBg}}>
|
||||
<KeyboardAvoidingView
|
||||
behavior='padding'
|
||||
style={{flex: 1, backgroundColor: theme.centerChannelBg}}
|
||||
>
|
||||
<StatusBar barStyle='default'/>
|
||||
<ChannelDrawer
|
||||
currentTeam={currentTeam}
|
||||
|
|
@ -93,35 +106,23 @@ export default class Channel extends React.Component {
|
|||
onCloseStart={this.closeRightSidebar}
|
||||
openDrawerOffset={0.2}
|
||||
>
|
||||
<View style={{backgroundColor: theme.sidebarHeaderBg, flexDirection: 'row', justifyContent: 'flex-start', marginTop: 20}}>
|
||||
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center'}}>
|
||||
<TouchableHighlight
|
||||
onPress={this.props.actions.openChannelDrawer}
|
||||
style={{height: 25, width: 25, marginLeft: 10, marginRight: 10}}
|
||||
>
|
||||
<Icon
|
||||
name='bars'
|
||||
size={25}
|
||||
color={theme.sidebarHeaderTextColor}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
<View style={{flex: 1, flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', backgroundColor: theme.sidebarHeaderBg}}>
|
||||
<Text style={{color: theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold'}}>
|
||||
{currentChannel.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableHighlight
|
||||
onPress={this.openRightSidebar}
|
||||
style={{height: 50, width: 50}}
|
||||
>
|
||||
<Text style={{color: theme.sidebarHeaderTextColor}}>{'>'}</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
<ChannelHeader
|
||||
currentChannel={currentChannel}
|
||||
openLeftDrawer={this.openChannelDrawer}
|
||||
openRightDrawer={this.openRightSidebar}
|
||||
/>
|
||||
<ChannelPostList channel={currentChannel}/>
|
||||
<Text value={this.props.postDraft}/>
|
||||
<PostTextbox
|
||||
ref='postTextbox'
|
||||
value={this.props.postDraft}
|
||||
teamId={currentChannel.team_id}
|
||||
channelId={currentChannel.id}
|
||||
onChangeText={this.props.actions.handlePostDraftChanged}
|
||||
/>
|
||||
</Drawer>
|
||||
</ChannelDrawer>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import {connect} from 'react-redux';
|
|||
import {
|
||||
loadChannelsIfNecessary,
|
||||
loadProfilesAndTeamMembersForDMSidebar,
|
||||
selectInitialChannel
|
||||
selectInitialChannel,
|
||||
handlePostDraftChanged
|
||||
} from 'app/actions/views/channel';
|
||||
import {openChannelDrawer} from 'app/actions/views/drawer';
|
||||
|
||||
|
|
@ -15,27 +16,14 @@ import {getCurrentChannel} from 'service/selectors/entities/channels';
|
|||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
import {getCurrentTeam} from 'service/selectors/entities/teams';
|
||||
|
||||
import {Constants} from 'service/constants';
|
||||
import {displayUsername} from 'service/utils/user_utils';
|
||||
import {getUserIdFromChannelName} from 'service/utils/channel_utils';
|
||||
|
||||
import Channel from './channel.js';
|
||||
import Channel from './channel';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const channel = getCurrentChannel(state);
|
||||
const currentChannel = {...channel};
|
||||
const {currentId, profiles} = state.entities.users;
|
||||
const {myPreferences} = state.entities.preferences;
|
||||
|
||||
if (channel && channel.type === Constants.DM_CHANNEL) {
|
||||
const otherUserId = getUserIdFromChannelName(currentId, currentChannel);
|
||||
currentChannel.display_name = displayUsername(profiles[otherUserId], myPreferences);
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
...state.views.channel,
|
||||
currentTeam: getCurrentTeam(state),
|
||||
currentChannel,
|
||||
currentChannel: getCurrentChannel(state),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
|
@ -46,7 +34,8 @@ function mapDispatchToProps(dispatch) {
|
|||
loadChannelsIfNecessary,
|
||||
loadProfilesAndTeamMembersForDMSidebar,
|
||||
selectInitialChannel,
|
||||
openChannelDrawer
|
||||
openChannelDrawer,
|
||||
handlePostDraftChanged
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
54
app/scenes/channel/channel_header/channel_header.js
Normal file
54
app/scenes/channel/channel_header/channel_header.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
export default class ChannelHeader extends React.PureComponent {
|
||||
static propTypes = {
|
||||
displayName: React.PropTypes.string.isRequired,
|
||||
theme: React.PropTypes.object.isRequired,
|
||||
openLeftDrawer: React.PropTypes.func.isRequired,
|
||||
openRightDrawer: React.PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
displayName,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<View style={{backgroundColor: theme.sidebarHeaderBg, flexDirection: 'row', justifyContent: 'flex-start', marginTop: 20}}>
|
||||
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center'}}>
|
||||
<TouchableHighlight
|
||||
onPress={this.props.openLeftDrawer}
|
||||
style={{height: 25, width: 25, marginLeft: 10, marginRight: 10}}
|
||||
>
|
||||
<Icon
|
||||
name='bars'
|
||||
size={25}
|
||||
color={theme.sidebarHeaderTextColor}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
<View style={{flex: 1, flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', backgroundColor: theme.sidebarHeaderBg}}>
|
||||
<Text style={{color: theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold'}}>
|
||||
{displayName}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableHighlight
|
||||
onPress={this.props.openRightDrawer}
|
||||
style={{height: 50, width: 50}}
|
||||
>
|
||||
<Text style={{color: theme.sidebarHeaderTextColor}}>{'>'}</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {Constants} from 'service/constants';
|
||||
import {getMyPreferences, getTheme} from 'service/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from 'service/selectors/entities/users';
|
||||
import {getUserIdFromChannelName} from 'service/utils/channel_utils';
|
||||
import {displayUsername} from 'service/utils/user_utils';
|
||||
|
||||
import ChannelHeader from './channel_header';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const currentChannel = ownProps.currentChannel;
|
||||
|
||||
let displayName = '';
|
||||
if (currentChannel) {
|
||||
if (currentChannel.type === Constants.DM_CHANNEL) {
|
||||
const otherUser = getUser(state, getUserIdFromChannelName(getCurrentUserId(state), currentChannel));
|
||||
|
||||
if (otherUser) {
|
||||
displayName = displayUsername(otherUser, getMyPreferences(state));
|
||||
}
|
||||
} else {
|
||||
displayName = currentChannel.display_name;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
displayName,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ChannelHeader);
|
||||
6
app/scenes/channel/channel_header/index.js
Normal file
6
app/scenes/channel/channel_header/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ChannelHeaderContainer from './channel_header_container';
|
||||
|
||||
export default ChannelHeaderContainer;
|
||||
6
app/scenes/channel/index.js
Normal file
6
app/scenes/channel/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ChannelContainer from './channel_container';
|
||||
|
||||
export default ChannelContainer;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import Channel from './channel/channel_container.js';
|
||||
import Channel from './channel';
|
||||
import Login from './login/login_container.js';
|
||||
import Root from './root/root_container.js';
|
||||
import Search from './search/search_container.js';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
|
|||
import {connect} from 'react-redux';
|
||||
|
||||
import {getClientConfig, getLicenseConfig} from 'service/actions/general';
|
||||
import * as LoginActions from 'app/actions/views/login';
|
||||
import LoginActions from 'app/actions/views/login';
|
||||
import * as StorageActions from 'app/actions/storage';
|
||||
import {goToSelectTeam} from 'app/actions/navigation';
|
||||
import {login} from 'service/actions/users';
|
||||
|
|
|
|||
24
app/utils/colors.js
Normal file
24
app/utils/colors.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
export function changeOpacity(oldColor, opacity) {
|
||||
let color = oldColor;
|
||||
if (color[0] === '#') {
|
||||
color = color.slice(1);
|
||||
}
|
||||
|
||||
if (color.length === 3) {
|
||||
const tempColor = color;
|
||||
color = '';
|
||||
|
||||
color += tempColor[0] + tempColor[0];
|
||||
color += tempColor[1] + tempColor[1];
|
||||
color += tempColor[2] + tempColor[2];
|
||||
}
|
||||
|
||||
const r = parseInt(color.substring(0, 2), 16);
|
||||
const g = parseInt(color.substring(2, 4), 16);
|
||||
const b = parseInt(color.substring(4, 6), 16);
|
||||
|
||||
return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
|
|
|||
|
|
@ -1,43 +1,52 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import Config from 'assets/config.json';
|
||||
import Themes from 'assets/themes.json';
|
||||
|
||||
import {Preferences} from 'service/constants';
|
||||
|
||||
export function getTheme(state) {
|
||||
const myPreferences = state.entities.preferences.myPreferences;
|
||||
const currentTeamId = state.entities.teams.currentId;
|
||||
import {getCurrentTeamId} from './teams';
|
||||
|
||||
// Prefer the user's current team-specific theme over the user's current global theme over the default theme
|
||||
let themePreference;
|
||||
if (currentTeamId) {
|
||||
themePreference = myPreferences[`${Preferences.CATEGORY_THEME}--${currentTeamId}`];
|
||||
}
|
||||
|
||||
if (!themePreference) {
|
||||
themePreference = myPreferences[`${Preferences.CATEGORY_THEME}--`];
|
||||
}
|
||||
|
||||
let theme;
|
||||
if (themePreference) {
|
||||
theme = themePreference.value;
|
||||
} else {
|
||||
theme = Config.DefaultTheme;
|
||||
}
|
||||
|
||||
if (typeof theme === 'string') {
|
||||
try {
|
||||
// A custom theme will be a JSON-serialized object stored in a preference
|
||||
theme = JSON.parse(theme);
|
||||
} catch (e) {
|
||||
// But if it's not a JSON object, it must be the name of a default theme
|
||||
theme = Themes[theme];
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, the theme should be a plain object
|
||||
|
||||
return theme;
|
||||
export function getMyPreferences(state) {
|
||||
return state.entities.preferences.myPreferences;
|
||||
}
|
||||
|
||||
export const getTheme = createSelector(
|
||||
getMyPreferences,
|
||||
getCurrentTeamId,
|
||||
(myPreferences, currentTeamId) => {
|
||||
// Prefer the user's current team-specific theme over the user's current global theme over the default theme
|
||||
let themePreference;
|
||||
if (currentTeamId) {
|
||||
themePreference = myPreferences[`${Preferences.CATEGORY_THEME}--${currentTeamId}`];
|
||||
}
|
||||
|
||||
if (!themePreference) {
|
||||
themePreference = myPreferences[`${Preferences.CATEGORY_THEME}--`];
|
||||
}
|
||||
|
||||
let theme;
|
||||
if (themePreference) {
|
||||
theme = themePreference.value;
|
||||
} else {
|
||||
theme = Config.DefaultTheme;
|
||||
}
|
||||
|
||||
if (typeof theme === 'string') {
|
||||
try {
|
||||
// A custom theme will be a JSON-serialized object stored in a preference
|
||||
theme = JSON.parse(theme);
|
||||
} catch (e) {
|
||||
// But if it's not a JSON object, it must be the name of a default theme
|
||||
theme = Themes[theme];
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, the theme should be a plain object
|
||||
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,24 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
export function getCurrentUserId(state) {
|
||||
return state.entities.users.currentId;
|
||||
}
|
||||
|
||||
export function getUsers(state) {
|
||||
return state.entities.users.profiles;
|
||||
}
|
||||
|
||||
export const getCurrentUser = createSelector(
|
||||
getUsers,
|
||||
getCurrentUserId,
|
||||
(profiles, currentUserId) => {
|
||||
return profiles[currentUserId];
|
||||
}
|
||||
);
|
||||
|
||||
export function getUser(state, id) {
|
||||
return state.entities.users.profiles[id];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue