Channel drawer (#154)
* Channel list drawer * Get channels by category * Fix and connect websocket * Fix Select first team using the team members * create loadMe and fix login actions to loads the teams * Add android back button handler for the channel drawer * Channel drawer styling and functionality * Mark channel as viewed and fixed reset unread counts * Unread above/below indicators * Improve performance replacing ScrollView with ListView * Fix unread indicators * Addressing review feedback
This commit is contained in:
parent
aa5a77ae86
commit
81526fbb09
47 changed files with 1724 additions and 253 deletions
33
NOTICE.txt
33
NOTICE.txt
|
|
@ -535,3 +535,36 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-svg
|
||||
|
||||
This product contains a modified portion of 'react-native-svg', library to provide a SVG interface to react native on both iOS and Android.
|
||||
|
||||
* HOMEPAGE
|
||||
* https://github.com/react-native-community/react-native-svg
|
||||
|
||||
* LICENSE:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) [2015-2016] [Horcrux]
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':react-native-svg')
|
||||
compile fileTree(dir: "libs", include: ["*.jar"])
|
||||
compile "com.android.support:appcompat-v7:23.0.1"
|
||||
compile "com.facebook.react:react-native:+" // From node_modules
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.app.Application;
|
|||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.horcrux.svg.RNSvgPackage;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
|
|
@ -23,7 +24,8 @@ public class MainApplication extends Application implements ReactApplication {
|
|||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
return Arrays.<ReactPackage>asList(
|
||||
new MainReactPackage()
|
||||
new MainReactPackage(),
|
||||
new RNSvgPackage()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
rootProject.name = 'Mattermost'
|
||||
|
||||
include ':app'
|
||||
include ':react-native-svg'
|
||||
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {fetchMyChannelsAndMembers, selectChannel} from 'service/actions/channels';
|
||||
import {fetchMyChannelsAndMembers, getMyChannelMembers, selectChannel} from 'service/actions/channels';
|
||||
import {getPosts} from 'service/actions/posts';
|
||||
import {Constants} from 'service/constants';
|
||||
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) => {
|
||||
|
|
@ -18,18 +22,66 @@ export function loadChannelsIfNecessary(teamId) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!hasChannelsForTeam) {
|
||||
if (hasChannelsForTeam) {
|
||||
await getMyChannelMembers(teamId)(dispatch, getState);
|
||||
} else {
|
||||
await fetchMyChannelsAndMembers(teamId)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = state.entities.users.currentId;
|
||||
const {channels} = state.entities.channels;
|
||||
const {myPreferences} = state.entities.preferences;
|
||||
const {membersInTeam} = state.entities.teams;
|
||||
const dmPrefs = getPreferencesByCategory(myPreferences, Constants.CATEGORY_DIRECT_CHANNEL_SHOW);
|
||||
const members = [];
|
||||
|
||||
for (const [key, pref] of dmPrefs) {
|
||||
if (pref.value === 'true') {
|
||||
members.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
let membersToLoad = members;
|
||||
if (membersInTeam[teamId]) {
|
||||
membersToLoad = members.filter((m) => !membersInTeam[teamId].has(m));
|
||||
}
|
||||
|
||||
await getTeamMembersByIds(teamId, membersToLoad)(dispatch, getState);
|
||||
|
||||
const actions = [];
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
const channelName = getDirectChannelName(currentUserId, members[i]);
|
||||
const channel = getChannelByName(channels, channelName);
|
||||
if (channel) {
|
||||
actions.push({
|
||||
type: UsersTypes.RECEIVED_PROFILE_IN_CHANNEL,
|
||||
data: {user_id: members[i]},
|
||||
id: channel.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions), getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadPostsIfNecessary(channel) {
|
||||
return async (dispatch, getState) => {
|
||||
const postsInChannel = getState().entities.posts.postsByChannel[channel.id];
|
||||
|
||||
if (!postsInChannel) {
|
||||
await getPosts(channel.team_id, channel.id)(dispatch, getState);
|
||||
let teamId = channel.team_id;
|
||||
if (!teamId) {
|
||||
teamId = getState().entities.teams.currentId;
|
||||
}
|
||||
await getPosts(teamId, channel.id)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
16
app/actions/views/drawer.js
Normal file
16
app/actions/views/drawer.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function openChannelDrawer() {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({type: ViewTypes.TOGGLE_CHANNEL_DRAWER, data: true}, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function closeChannelDrawer() {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({type: ViewTypes.TOGGLE_CHANNEL_DRAWER, data: false}, getState);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import Routes from 'app/navigation/routes';
|
||||
|
||||
import Client from 'service/client';
|
||||
import {forceLogoutIfNecessary} from 'service/actions/helpers';
|
||||
import {PreferencesTypes, TeamsTypes, UsersTypes} from 'service/constants';
|
||||
import {loadMe} from 'service/actions/users';
|
||||
|
||||
export function goToSelectServer() {
|
||||
return async (dispatch, getState) => {
|
||||
|
|
@ -24,76 +22,7 @@ export function setStoreFromLocalData(data) {
|
|||
Client.setToken(data.token);
|
||||
Client.setUrl(data.url);
|
||||
|
||||
let user;
|
||||
dispatch({type: UsersTypes.LOGIN_REQUEST}, getState);
|
||||
try {
|
||||
user = await Client.getMe();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: UsersTypes.LOGIN_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
let preferences;
|
||||
dispatch({type: PreferencesTypes.MY_PREFERENCES_REQUEST}, getState);
|
||||
try {
|
||||
preferences = await Client.getMyPreferences();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: PreferencesTypes.MY_PREFERENCES_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
let teams;
|
||||
dispatch({type: TeamsTypes.FETCH_TEAMS_REQUEST}, getState);
|
||||
try {
|
||||
teams = await Client.getAllTeams();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: TeamsTypes.FETCH_TEAMS_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
let teamMembers;
|
||||
dispatch({type: TeamsTypes.MY_TEAM_MEMBERS_REQUEST}, getState);
|
||||
try {
|
||||
teamMembers = await Client.getMyTeamMembers();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: TeamsTypes.MY_TEAM_MEMBERS_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: UsersTypes.RECEIVED_ME,
|
||||
data: user
|
||||
},
|
||||
{
|
||||
type: UsersTypes.LOGIN_SUCCESS
|
||||
},
|
||||
{
|
||||
type: PreferencesTypes.RECEIVED_PREFERENCES,
|
||||
data: preferences
|
||||
},
|
||||
{
|
||||
type: PreferencesTypes.MY_PREFERENCES_SUCCESS
|
||||
},
|
||||
{
|
||||
type: TeamsTypes.RECEIVED_MY_TEAM_MEMBERS,
|
||||
data: teamMembers
|
||||
},
|
||||
{
|
||||
type: TeamsTypes.MY_TEAM_MEMBERS_SUCCESS
|
||||
},
|
||||
{
|
||||
type: TeamsTypes.RECEIVED_ALL_TEAMS,
|
||||
data: teams
|
||||
},
|
||||
{
|
||||
type: TeamsTypes.FETCH_TEAMS_SUCCESS
|
||||
}
|
||||
]), getState);
|
||||
return loadMe()(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
93
app/components/channel_drawer/badge.js
Normal file
93
app/components/channel_drawer/badge.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View, Text, StyleSheet} from 'react-native';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
text: {
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
export default class Badge extends React.Component {
|
||||
static defaultProps = {
|
||||
extraPaddingHorizontal: 10,
|
||||
minHeight: 0,
|
||||
minWidth: 0
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
count: React.PropTypes.number.isRequired,
|
||||
extraPaddingHorizontal: React.PropTypes.number,
|
||||
style: View.propTypes.style,
|
||||
countStyle: Text.propTypes.style,
|
||||
minHeight: React.PropTypes.number,
|
||||
minWidth: React.PropTypes.number
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.width = 0;
|
||||
}
|
||||
|
||||
renderText = () => {
|
||||
return (
|
||||
<Text
|
||||
onLayout={this.onLayout}
|
||||
style={[styles.text, this.props.countStyle]}
|
||||
>
|
||||
{this.props.count}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
onLayout = (e) => {
|
||||
let width;
|
||||
|
||||
if (e.nativeEvent.layout.width <= e.nativeEvent.layout.height) {
|
||||
width = e.nativeEvent.layout.height;
|
||||
} else {
|
||||
width = e.nativeEvent.layout.width + this.props.extraPaddingHorizontal;
|
||||
}
|
||||
|
||||
width = Math.max(width, this.props.minWidth);
|
||||
if (this.width === width) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.width = width;
|
||||
const height = Math.max(e.nativeEvent.layout.height, this.props.minHeight);
|
||||
const borderRadius = height / 2;
|
||||
this.container.setNativeProps({
|
||||
style: {
|
||||
width,
|
||||
height,
|
||||
borderRadius
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View
|
||||
ref={(component) => {
|
||||
this.container = component;
|
||||
}}
|
||||
style={[styles.container, this.props.style]}
|
||||
>
|
||||
{this.renderText()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
102
app/components/channel_drawer/channel_drawer.js
Normal file
102
app/components/channel_drawer/channel_drawer.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Platform, BackAndroid} from 'react-native';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
import Drawer from 'react-native-drawer';
|
||||
import ChannelList from './channel_list';
|
||||
|
||||
export default class ChannelDrawer extends React.Component {
|
||||
static propTypes = {
|
||||
children: React.PropTypes.element.isRequired,
|
||||
actions: React.PropTypes.shape({
|
||||
selectChannel: React.PropTypes.func.isRequired,
|
||||
viewChannel: React.PropTypes.func.isRequired,
|
||||
closeChannelDrawer: React.PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
currentTeam: React.PropTypes.object,
|
||||
currentChannelId: React.PropTypes.string,
|
||||
channels: React.PropTypes.object,
|
||||
channelMembers: React.PropTypes.object,
|
||||
theme: React.PropTypes.object.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
this.handleBackButton = this.handleBackButton.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (Platform.OS === 'android') {
|
||||
BackAndroid.addEventListener('hardwareBackPress', this.handleBackButton);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (Platform.OS === 'android') {
|
||||
BackAndroid.removeEventListener('hardwareBackPress', this.handleBackButton);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.isOpen && nextProps.isOpen) {
|
||||
this.drawer.open();
|
||||
} else if (this.props.isOpen && !nextProps.isOpen) {
|
||||
this.drawer.close();
|
||||
}
|
||||
}
|
||||
|
||||
handleBackButton() {
|
||||
if (this.props.isOpen) {
|
||||
this.props.actions.closeChannelDrawer();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentChannelId,
|
||||
currentTeam,
|
||||
channels,
|
||||
channelMembers,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
ref={(ref) => {
|
||||
this.drawer = ref;
|
||||
}}
|
||||
onClose={this.props.actions.closeChannelDrawer}
|
||||
type='displace'
|
||||
openDrawerOffset={0.2}
|
||||
closedDrawerOffset={0}
|
||||
panOpenMask={0.1}
|
||||
panCloseMask={0.2}
|
||||
panThreshold={0.2}
|
||||
acceptPan={true}
|
||||
tapToClose={true}
|
||||
content={
|
||||
<ChannelList
|
||||
currentTeam={currentTeam}
|
||||
currentChannelId={currentChannelId}
|
||||
channels={channels}
|
||||
channelMembers={channelMembers}
|
||||
theme={theme}
|
||||
onSelectChannel={this.props.actions.selectChannel}
|
||||
onViewChannel={this.props.actions.viewChannel}
|
||||
closeChannelDrawer={this.props.actions.closeChannelDrawer}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{this.props.children}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
||||
33
app/components/channel_drawer/channel_drawer_container.js
Normal file
33
app/components/channel_drawer/channel_drawer_container.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {selectChannel, viewChannel} from 'service/actions/channels';
|
||||
import {getChannelsByCategory} from 'service/selectors/entities/channels';
|
||||
import {closeChannelDrawer} from 'app/actions/views/drawer';
|
||||
import ChannelDrawer from './channel_drawer';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const isOpen = state.views.drawer.channel;
|
||||
const channelMembers = state.entities.channels.myMembers;
|
||||
return {
|
||||
...ownProps,
|
||||
channels: getChannelsByCategory(state),
|
||||
channelMembers,
|
||||
isOpen
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
selectChannel,
|
||||
viewChannel,
|
||||
closeChannelDrawer
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawer);
|
||||
192
app/components/channel_drawer/channel_item.js
Normal file
192
app/components/channel_drawer/channel_item.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {OnlineStatus, AwayStatus, OfflineStatus} from 'app/components/status_icons';
|
||||
import {Constants} from 'service/constants';
|
||||
|
||||
export default class ChannelItem extends React.Component {
|
||||
static propTypes = {
|
||||
channel: React.PropTypes.object.isRequired,
|
||||
onSelectChannel: React.PropTypes.func.isRequired,
|
||||
isActive: React.PropTypes.bool.isRequired,
|
||||
hasUnread: React.PropTypes.bool.isRequired,
|
||||
mentions: React.PropTypes.number.isRequired,
|
||||
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,
|
||||
theme,
|
||||
mentions,
|
||||
hasUnread,
|
||||
isActive
|
||||
} = this.props;
|
||||
|
||||
let iconColor = this.changeOpacity(theme.centerChannelColor, 0.7);
|
||||
let icon;
|
||||
let activeBorder;
|
||||
let badge;
|
||||
|
||||
if (mentions && !isActive) {
|
||||
const badgeStyle = {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: theme.mentionBj
|
||||
};
|
||||
|
||||
const mentionStyle = {
|
||||
color: theme.mentionColor,
|
||||
fontSize: 14
|
||||
};
|
||||
|
||||
badge = (
|
||||
<Badge
|
||||
style={badgeStyle}
|
||||
countStyle={mentionStyle}
|
||||
count={mentions}
|
||||
minHeight={20}
|
||||
minWidth={20}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const itemStyle = {
|
||||
alignItems: 'center',
|
||||
height: 40,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 10,
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
};
|
||||
|
||||
const style = {
|
||||
marginLeft: 5,
|
||||
opacity: 0.6,
|
||||
color: theme.sidebarText
|
||||
};
|
||||
|
||||
const activeStyle = {
|
||||
width: 5,
|
||||
height: 40,
|
||||
backgroundColor: theme.sidebarTextActiveBorder,
|
||||
position: 'absolute'
|
||||
};
|
||||
|
||||
if (hasUnread) {
|
||||
style.fontWeight = 'bold';
|
||||
style.color = theme.sidebarUnreadText;
|
||||
style.opacity = 1;
|
||||
iconColor = theme.centerChannelColor;
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
iconColor = theme.sidebarTextActiveColor;
|
||||
style.color = theme.sidebarTextActiveColor;
|
||||
style.opacity = 1;
|
||||
itemStyle.backgroundColor = this.changeOpacity(theme.sidebarTextActiveColor, 0.1);
|
||||
|
||||
activeBorder = (
|
||||
<View style={activeStyle}/>
|
||||
);
|
||||
}
|
||||
|
||||
if (channel.type === Constants.OPEN_CHANNEL) {
|
||||
icon = (
|
||||
<Icon
|
||||
name='globe'
|
||||
size={15}
|
||||
color={iconColor}
|
||||
/>
|
||||
);
|
||||
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
icon = (
|
||||
<Icon
|
||||
name='lock'
|
||||
size={15}
|
||||
color={iconColor}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
switch (channel.status) {
|
||||
case Constants.ONLINE:
|
||||
icon = (
|
||||
<OnlineStatus
|
||||
width={13}
|
||||
height={13}
|
||||
color={theme.onlineIndicator}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case Constants.AWAY:
|
||||
icon = (
|
||||
<AwayStatus
|
||||
width={13}
|
||||
height={13}
|
||||
color={theme.awayIndicator}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
icon = (
|
||||
<OfflineStatus
|
||||
width={13}
|
||||
height={13}
|
||||
color={this.changeOpacity(theme.centerChannelColor, 0.7)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
underlayColor={this.changeOpacity(theme.sidebarTextHoverBg, 0.3)}
|
||||
onPress={() => this.props.onSelectChannel(channel)}
|
||||
>
|
||||
<View style={{flex: 1}}>
|
||||
{activeBorder}
|
||||
<View style={itemStyle}>
|
||||
{icon}
|
||||
<Text
|
||||
style={style}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{channel.display_name}
|
||||
</Text>
|
||||
{badge}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
321
app/components/channel_drawer/channel_list.js
Normal file
321
app/components/channel_drawer/channel_list.js
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {StyleSheet, Text, View, ListView} from 'react-native';
|
||||
import LineDivider from 'app/components/line_divider';
|
||||
import ChannelItem from './channel_item';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import UnreadIndicator from './unread_indicator';
|
||||
|
||||
const Styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginTop: 20
|
||||
},
|
||||
scrollContainer: {
|
||||
flex: 1
|
||||
},
|
||||
headerContainer: {
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
height: 50,
|
||||
width: 300,
|
||||
paddingLeft: 10
|
||||
},
|
||||
header: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
title: {
|
||||
paddingTop: 10,
|
||||
paddingRight: 10,
|
||||
paddingLeft: 10,
|
||||
paddingBottom: 5,
|
||||
fontSize: 15,
|
||||
opacity: 0.6
|
||||
},
|
||||
indicatorText: {
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
export default class ChannelList extends React.Component {
|
||||
static propTypes = {
|
||||
currentTeam: React.PropTypes.object.isRequired,
|
||||
currentChannelId: React.PropTypes.string,
|
||||
channels: React.PropTypes.object.isRequired,
|
||||
channelMembers: React.PropTypes.object,
|
||||
theme: React.PropTypes.object.isRequired,
|
||||
onSelectChannel: React.PropTypes.func.isRequired,
|
||||
onViewChannel: React.PropTypes.func.isRequired,
|
||||
closeChannelDrawer: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.firstUnreadChannel = null;
|
||||
this.lastUnreadChannel = null;
|
||||
this.state = {
|
||||
showAbove: false,
|
||||
showBelow: false,
|
||||
dataSource: new ListView.DataSource({
|
||||
rowHasChanged: (a, b) => a !== b
|
||||
}).cloneWithRows(props)
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
dataSource: this.state.dataSource.cloneWithRows(this.buildData(nextProps))
|
||||
});
|
||||
const container = this.refs.scrollContainer;
|
||||
if (container && container._visibleRows && container._visibleRows.s1) { //eslint-disable-line no-underscore-dangle
|
||||
this.updateUnreadIndicators(container._visibleRows); //eslint-disable-line no-underscore-dangle
|
||||
}
|
||||
}
|
||||
|
||||
updateUnreadIndicators = (v) => {
|
||||
const data = this.state.dataSource._dataBlob.s1; //eslint-disable-line no-underscore-dangle
|
||||
|
||||
if (this.firstUnreadChannel) {
|
||||
const index = data.findIndex((obj) => obj.id === this.firstUnreadChannel);
|
||||
if (v.s1[index]) {
|
||||
this.setState({
|
||||
showAbove: false
|
||||
});
|
||||
} else if (!v.s1[index - 1]) {
|
||||
this.setState({
|
||||
showAbove: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.lastUnreadChannel) {
|
||||
const index = data.findIndex((obj) => obj.id === this.lastUnreadChannel);
|
||||
if (v.s1[index]) {
|
||||
this.setState({
|
||||
showBelow: false
|
||||
});
|
||||
} else if (!v.s1[index + 1]) {
|
||||
this.setState({
|
||||
showBelow: true
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onSelectChannel = (channel) => {
|
||||
console.log('clicked channel ' + channel.name); // eslint-disable-line no-console
|
||||
|
||||
const {
|
||||
currentChannelId,
|
||||
currentTeam
|
||||
} = this.props;
|
||||
|
||||
this.props.onSelectChannel(channel.id);
|
||||
this.props.onViewChannel(currentTeam.id, channel.id, currentChannelId);
|
||||
this.props.closeChannelDrawer();
|
||||
};
|
||||
|
||||
getUnreadMessages = (channel) => {
|
||||
const member = this.props.channelMembers[channel.id];
|
||||
const mentions = member.mention_count;
|
||||
let unreadCount = channel.total_msg_count - member.msg_count;
|
||||
|
||||
if (member.notify_props && member.notify_props.mark_unread === 'mention') {
|
||||
unreadCount = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
mentions,
|
||||
unreadCount
|
||||
};
|
||||
};
|
||||
|
||||
findUnreadChannels = (data) => {
|
||||
data.forEach((c) => {
|
||||
if (c.id) {
|
||||
const {mentions, unreadCount} = this.getUnreadMessages(c);
|
||||
const unread = (mentions + unreadCount) > 0;
|
||||
|
||||
if (unread && c.id !== this.props.currentChannelId) {
|
||||
if (this.firstUnreadChannel) {
|
||||
this.lastUnreadChannel = c.id;
|
||||
} else {
|
||||
this.firstUnreadChannel = c.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
createChannelElement = (channel) => {
|
||||
const {mentions, unreadCount} = this.getUnreadMessages(channel);
|
||||
const msgCount = mentions + unreadCount;
|
||||
const unread = msgCount > 0;
|
||||
|
||||
return (
|
||||
<ChannelItem
|
||||
ref={channel.id}
|
||||
key={channel.id}
|
||||
channel={channel}
|
||||
hasUnread={unread}
|
||||
mentions={mentions}
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
isActive={channel.id === this.props.currentChannelId}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
buildData = (props) => {
|
||||
const data = [];
|
||||
|
||||
if (!props.currentChannelId) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const {
|
||||
theme
|
||||
} = props;
|
||||
|
||||
const {
|
||||
favoriteChannels,
|
||||
publicChannels,
|
||||
privateChannels,
|
||||
directChannels,
|
||||
directNonTeamChannels
|
||||
} = props.channels;
|
||||
|
||||
if (favoriteChannels.length) {
|
||||
data.push(
|
||||
<FormattedText
|
||||
style={[Styles.title, {color: theme.sidebarText}]}
|
||||
id='sidebar.favorite'
|
||||
defaultMessage='FAVORITES'
|
||||
/>,
|
||||
...favoriteChannels
|
||||
);
|
||||
}
|
||||
|
||||
data.push(
|
||||
<FormattedText
|
||||
style={[Styles.title, {color: theme.sidebarText}]}
|
||||
id='sidebar.channels'
|
||||
defaultMessage='CHANNELS'
|
||||
/>,
|
||||
...publicChannels
|
||||
);
|
||||
data.push(
|
||||
<FormattedText
|
||||
style={[Styles.title, {color: theme.sidebarText}]}
|
||||
id='sidebar.pg'
|
||||
defaultMessage='PRIVATE GROUPS'
|
||||
/>,
|
||||
...privateChannels
|
||||
);
|
||||
data.push(
|
||||
<FormattedText
|
||||
style={[Styles.title, {color: theme.sidebarText}]}
|
||||
id='sidebar.direct'
|
||||
defaultMessage='DIRECT MESSAGES'
|
||||
/>,
|
||||
...directChannels
|
||||
);
|
||||
|
||||
if (directNonTeamChannels.length) {
|
||||
data.push(
|
||||
<LineDivider
|
||||
color={theme.sidebarTextActiveBorder}
|
||||
translationId='sidebar.otherMembers'
|
||||
translationText='Outside this team'
|
||||
/>,
|
||||
...directNonTeamChannels
|
||||
);
|
||||
}
|
||||
|
||||
this.firstUnreadChannel = null;
|
||||
this.lastUnreadChannel = null;
|
||||
this.findUnreadChannels(data);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
renderRow = (rowData, sectionId, rowId) => {
|
||||
if (rowData && rowData.id) {
|
||||
return this.createChannelElement(rowData, sectionId, rowId);
|
||||
}
|
||||
return rowData;
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.currentChannelId) {
|
||||
return <Text>{'Loading'}</Text>;
|
||||
}
|
||||
|
||||
const {
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
let above;
|
||||
let below;
|
||||
if (this.state.showAbove) {
|
||||
above = (
|
||||
<UnreadIndicator
|
||||
style={{top: 55, backgroundColor: theme.mentionBj}}
|
||||
text={(
|
||||
<FormattedText
|
||||
style={[Styles.indicatorText, {color: theme.mentionColor}]}
|
||||
id='sidebar.unreadAbove'
|
||||
defaultMessage='Unread post(s) above'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (this.state.showBelow) {
|
||||
below = (
|
||||
<UnreadIndicator
|
||||
style={{bottom: 15, backgroundColor: theme.mentionBj}}
|
||||
text={(
|
||||
<FormattedText
|
||||
style={[Styles.indicatorText, {color: theme.mentionColor}]}
|
||||
id='sidebar.unreadBelow'
|
||||
defaultMessage='Unread post(s) below'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[Styles.container, {backgroundColor: theme.sidebarBg}]}>
|
||||
<View style={[Styles.headerContainer, {backgroundColor: theme.sidebarHeaderBg}]}>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={[Styles.header, {color: theme.sidebarHeaderTextColor}]}
|
||||
>
|
||||
{this.props.currentTeam.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
<ListView
|
||||
ref='scrollContainer'
|
||||
style={Styles.scrollContainer}
|
||||
dataSource={this.state.dataSource}
|
||||
renderRow={this.renderRow}
|
||||
onChangeVisibleRows={this.updateUnreadIndicators}
|
||||
/>
|
||||
{above}
|
||||
{below}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
6
app/components/channel_drawer/index.js
Normal file
6
app/components/channel_drawer/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ChannelDrawerContainer from './channel_drawer_container';
|
||||
|
||||
export default ChannelDrawerContainer;
|
||||
43
app/components/channel_drawer/unread_indicator.js
Normal file
43
app/components/channel_drawer/unread_indicator.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
const Styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
position: 'absolute',
|
||||
borderRadius: 15,
|
||||
marginLeft: 25,
|
||||
width: 250,
|
||||
height: 25
|
||||
}
|
||||
});
|
||||
|
||||
export default class UnreadIndicator extends React.Component {
|
||||
static propTypes = {
|
||||
style: View.propTypes.style,
|
||||
textStyle: Text.propTypes.style,
|
||||
text: React.PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View
|
||||
style={[Styles.container, this.props.style]}
|
||||
>
|
||||
{this.props.text}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
class ChannelSidebar extends React.Component {
|
||||
static propTypes = {
|
||||
actions: React.PropTypes.shape({
|
||||
selectChannel: React.PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
currentTeam: React.PropTypes.object.isRequired,
|
||||
channels: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
onSelectChannel = (channel) => {
|
||||
console.log('clicked channel ' + channel.name); // eslint-disable-line no-console
|
||||
|
||||
this.props.actions.selectChannel(channel.id);
|
||||
}
|
||||
|
||||
render() {
|
||||
const channels = this.props.channels.map((channel) => {
|
||||
return (
|
||||
<Text
|
||||
key={channel.id}
|
||||
style={{height: 30, width: 100}}
|
||||
onPress={() => this.onSelectChannel(channel)}
|
||||
>
|
||||
{channel.name}
|
||||
</Text>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: 'gray',
|
||||
flex: 1,
|
||||
paddingTop: 20
|
||||
}}
|
||||
>
|
||||
<Text style={{height: 60, width: 100}}>{this.props.currentTeam.name}</Text>
|
||||
{channels}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {selectChannel} from 'service/actions/channels';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return ownProps;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
selectChannel
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelSidebar);
|
||||
83
app/components/line_divider.js
Normal file
83
app/components/line_divider.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
const Styles = StyleSheet.create({
|
||||
dividerLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: '#b3b3b3'
|
||||
},
|
||||
dividerContainer: {
|
||||
height: 20,
|
||||
marginLeft: 15,
|
||||
marginRight: 15
|
||||
},
|
||||
dividerText: {
|
||||
flex: 1,
|
||||
textAlign: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
export default class LineDivider extends React.Component {
|
||||
static propTypes = {
|
||||
color: React.PropTypes.string.isRequired,
|
||||
translationId: React.PropTypes.string,
|
||||
translationText: React.PropTypes.string,
|
||||
side: React.PropTypes.oneOf(['left', 'right', 'center'])
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
side: 'right'
|
||||
};
|
||||
|
||||
renderLine() {
|
||||
return (
|
||||
<View style={[Styles.dividerLine, {backgroundColor: this.props.color}]}/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let renderText;
|
||||
if (this.props.translationId && this.props.translationText) {
|
||||
renderText = (
|
||||
<View style={Styles.dividerContainer}>
|
||||
<FormattedText
|
||||
style={[Styles.dividerText, {color: this.props.color}]}
|
||||
id={this.props.translationId}
|
||||
defaultMessage={this.props.translationText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let content = (
|
||||
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
||||
{this.renderLine()}
|
||||
{renderText}
|
||||
</View>
|
||||
);
|
||||
if (this.props.side === 'left') {
|
||||
content = (
|
||||
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
||||
{renderText}
|
||||
{this.renderLine()}
|
||||
</View>
|
||||
);
|
||||
} else if (this.props.side === 'center') {
|
||||
content = (
|
||||
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
||||
{this.renderLine()}
|
||||
{renderText}
|
||||
{this.renderLine()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
45
app/components/status_icons/away.js
Normal file
45
app/components/status_icons/away.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import Svg, {
|
||||
Ellipse,
|
||||
G,
|
||||
Path
|
||||
} from 'react-native-svg';
|
||||
|
||||
export default class AwayStatus extends React.Component {
|
||||
static propTypes = {
|
||||
width: React.PropTypes.number.isRequired,
|
||||
height: React.PropTypes.number.isRequired,
|
||||
color: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Svg
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
viewBox='-299 391 12 12'
|
||||
>
|
||||
<G>
|
||||
<Ellipse
|
||||
cx='-294.6'
|
||||
cy='394'
|
||||
rx='2.5'
|
||||
ry='2.5'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
<Path
|
||||
d='M-293.8,399.4c0-0.4,0.1-0.7,0.2-1c-0.3,0.1-0.6,0.2-1,0.2c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0.7,0,1.4-0.1,2-0.3 C-293.3,401.5-293.8,400.5-293.8,399.4z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
<Path
|
||||
d='M-287,400c0,0.1-0.1,0.1-0.1,0.1l-4.9,0c-0.1,0-0.1-0.1-0.1-0.1v-1.6c0-0.1,0.1-0.1,0.1-0.1l4.9,0c0.1,0,0.1,0.1,0.1,0.1 V400z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
12
app/components/status_icons/index.js
Normal file
12
app/components/status_icons/index.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import OnlineStatus from './online';
|
||||
import AwayStatus from './away';
|
||||
import OfflineStatus from './offline';
|
||||
|
||||
export {
|
||||
OnlineStatus,
|
||||
AwayStatus,
|
||||
OfflineStatus
|
||||
};
|
||||
49
app/components/status_icons/offline.js
Normal file
49
app/components/status_icons/offline.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import Svg, {
|
||||
Ellipse,
|
||||
G,
|
||||
Path
|
||||
} from 'react-native-svg';
|
||||
|
||||
export default class OfflineStatus extends React.Component {
|
||||
static propTypes = {
|
||||
width: React.PropTypes.number.isRequired,
|
||||
height: React.PropTypes.number.isRequired,
|
||||
color: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Svg
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
viewBox='-299 391 12 12'
|
||||
>
|
||||
<G>
|
||||
<G>
|
||||
<Ellipse
|
||||
cx='-294.5'
|
||||
cy='394'
|
||||
rx='2.5'
|
||||
ry='2.5'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
<Path
|
||||
d='M-294.3,399.7c0-0.4,0.1-0.8,0.2-1.2c-0.1,0-0.2,0-0.4,0c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4h0.1h0.1c0.3,0,0.7,0,1-0.1C-293.9,401.6-294.3,400.7-294.3,399.7z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
</G>
|
||||
<G>
|
||||
<Path
|
||||
d='M-288.9,399.4l1.8-1.8c0.1-0.1,0.1-0.3,0-0.3l-0.7-0.7c-0.1-0.1-0.3-0.1-0.3,0l-1.8,1.8l-1.8-1.8c-0.1-0.1-0.3-0.1-0.3,0 l-0.7,0.7c-0.1,0.1-0.1,0.3,0,0.3l1.8,1.8l-1.8,1.8c-0.1,0.1-0.1,0.3,0,0.3l0.7,0.7c0.1,0.1,0.3,0.1,0.3,0l1.8-1.8l1.8,1.8 c0.1,0.1,0.3,0.1,0.3,0l0.7-0.7c0.1-0.1,0.1-0.3,0-0.3L-288.9,399.4z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
53
app/components/status_icons/online.js
Normal file
53
app/components/status_icons/online.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import Svg, {
|
||||
Ellipse,
|
||||
G,
|
||||
Path
|
||||
} from 'react-native-svg';
|
||||
|
||||
export default class OnlineStatus extends React.Component {
|
||||
static propTypes = {
|
||||
width: React.PropTypes.number.isRequired,
|
||||
height: React.PropTypes.number.isRequired,
|
||||
color: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Svg
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
viewBox='-243 245 12 12'
|
||||
>
|
||||
<G>
|
||||
<Path
|
||||
d='M-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
<Ellipse
|
||||
cx='-238.5'
|
||||
cy='248'
|
||||
rx='2.5'
|
||||
ry='2.5'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
<Path
|
||||
d='M-238.9,253.8c0-0.4,0.1-0.9,0.2-1.3c-2.2-0.2-2.2-2-2.2-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5c0,0.1-0.1,0.5,0,0.6 c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0C-238.7,255.7-238.9,254.8-238.9,253.8z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
<G>
|
||||
<G>
|
||||
<Path
|
||||
d='M-232.3,250.1l1.3,1.3c0,0,0,0.1,0,0.1l-4.1,4.1c0,0,0,0-0.1,0c0,0,0,0,0,0l-2.7-2.7c0,0,0-0.1,0-0.1l1.2-1.2 c0,0,0.1,0,0.1,0l1.4,1.4l2.9-2.9C-232.4,250.1-232.3,250.1-232.3,250.1z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,9 @@ const ViewTypes = keyMirror({
|
|||
SERVER_URL_CHANGED: null,
|
||||
|
||||
LOGIN_ID_CHANGED: null,
|
||||
PASSWORD_CHANGED: null
|
||||
PASSWORD_CHANGED: null,
|
||||
|
||||
TOGGLE_CHANNEL_DRAWER: null
|
||||
});
|
||||
|
||||
export default ViewTypes;
|
||||
|
|
|
|||
|
|
@ -43,10 +43,8 @@ class Router extends React.Component {
|
|||
<NavigationExperimental.Card
|
||||
{...cardProps}
|
||||
style={NavigationExperimental.Card.PagerStyleInterpolator.forHorizontal({
|
||||
...cardProps,
|
||||
onNavigateBack: this.props.actions.goBack
|
||||
...cardProps
|
||||
})}
|
||||
onNavigateBack={this.props.actions.goBack}
|
||||
renderScene={this.renderScene}
|
||||
key={scene.key}
|
||||
/>
|
||||
|
|
|
|||
20
app/reducers/views/drawer.js
Normal file
20
app/reducers/views/drawer.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
function channel(state = false, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.TOGGLE_CHANNEL_DRAWER:
|
||||
return action.data;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
channel
|
||||
});
|
||||
|
||||
|
|
@ -6,9 +6,11 @@ import {combineReducers} from 'redux';
|
|||
import login from './login';
|
||||
import selectServer from './select_server';
|
||||
import i18n from './i18n';
|
||||
import drawer from './drawer';
|
||||
|
||||
export default combineReducers({
|
||||
i18n,
|
||||
login,
|
||||
selectServer
|
||||
selectServer,
|
||||
drawer
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
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 Drawer from 'react-native-drawer';
|
||||
|
||||
import ChannelSidebar from 'app/components/channel_sidebar';
|
||||
import ChannelDrawer from 'app/components/channel_drawer';
|
||||
import RightSidebarMenu from 'app/components/right_sidebar_menu';
|
||||
|
||||
import ChannelPostList from './components/channel_post_list';
|
||||
|
|
@ -15,11 +16,12 @@ export default class Channel extends React.Component {
|
|||
static propTypes = {
|
||||
actions: React.PropTypes.shape({
|
||||
loadChannelsIfNecessary: React.PropTypes.func.isRequired,
|
||||
selectInitialChannel: React.PropTypes.func.isRequired
|
||||
loadProfilesAndTeamMembersForDMSidebar: React.PropTypes.func.isRequired,
|
||||
selectInitialChannel: React.PropTypes.func.isRequired,
|
||||
openChannelDrawer: React.PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
currentTeam: React.PropTypes.object.isRequired,
|
||||
currentTeam: React.PropTypes.object,
|
||||
currentChannel: React.PropTypes.object,
|
||||
channels: React.PropTypes.array,
|
||||
theme: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
|
@ -35,25 +37,22 @@ export default class Channel extends React.Component {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.actions.loadChannelsIfNecessary(this.props.currentTeam.id).then(() => {
|
||||
return this.props.actions.selectInitialChannel(this.props.currentTeam.id);
|
||||
});
|
||||
const teamId = this.props.currentTeam.id;
|
||||
this.loadChannels(teamId);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.currentTeam.id !== nextProps.currentTeam.id) {
|
||||
this.props.actions.loadChannelsIfNecessary(nextProps.currentTeam.id).then(() => {
|
||||
this.props.actions.selectInitialChannel(nextProps.currentTeam.id);
|
||||
});
|
||||
if (nextProps.currentTeam && this.props.currentTeam.id !== nextProps.currentTeam.id) {
|
||||
const teamId = nextProps.currentTeam.id;
|
||||
this.loadChannels(teamId);
|
||||
}
|
||||
}
|
||||
|
||||
openLeftSidebar = () => {
|
||||
this.setState({leftSidebarOpen: true});
|
||||
};
|
||||
|
||||
closeLeftSidebar = () => {
|
||||
this.setState({leftSidebarOpen: false});
|
||||
loadChannels = (teamId) => {
|
||||
this.props.actions.loadChannelsIfNecessary(teamId).then(() => {
|
||||
this.props.actions.loadProfilesAndTeamMembersForDMSidebar(teamId);
|
||||
return this.props.actions.selectInitialChannel(teamId);
|
||||
});
|
||||
};
|
||||
|
||||
openRightSidebar = () => {
|
||||
|
|
@ -68,7 +67,6 @@ export default class Channel extends React.Component {
|
|||
const {
|
||||
currentTeam,
|
||||
currentChannel,
|
||||
channels,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
|
|
@ -81,19 +79,10 @@ export default class Channel extends React.Component {
|
|||
return (
|
||||
<View style={{flex: 1, backgroundColor: theme.centerChannelBg}}>
|
||||
<StatusBar barStyle='default'/>
|
||||
<Drawer
|
||||
open={this.state.leftSidebarOpen}
|
||||
type='displace'
|
||||
content={
|
||||
<ChannelSidebar
|
||||
currentTeam={currentTeam}
|
||||
channels={channels}
|
||||
/>
|
||||
}
|
||||
side='left'
|
||||
tapToClose={true}
|
||||
onCloseStart={this.closeLeftSidebar}
|
||||
openDrawerOffset={0.2}
|
||||
<ChannelDrawer
|
||||
currentTeam={currentTeam}
|
||||
currentChannelId={currentChannel.id}
|
||||
theme={theme}
|
||||
>
|
||||
<Drawer
|
||||
open={this.state.rightSidebarOpen}
|
||||
|
|
@ -104,13 +93,24 @@ export default class Channel extends React.Component {
|
|||
onCloseStart={this.closeRightSidebar}
|
||||
openDrawerOffset={0.2}
|
||||
>
|
||||
<View style={{backgroundColor: theme.sidebarHeaderBg, flexDirection: 'row', justifyContent: 'space-between', marginTop: 20}}>
|
||||
<TouchableHighlight
|
||||
onPress={this.openLeftSidebar}
|
||||
style={{height: 50, width: 50}}
|
||||
>
|
||||
<Text style={{color: theme.sidebarHeaderTextColor}}>{'<'}</Text>
|
||||
</TouchableHighlight>
|
||||
<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}}
|
||||
|
|
@ -120,7 +120,7 @@ export default class Channel extends React.Component {
|
|||
</View>
|
||||
<ChannelPostList channel={currentChannel}/>
|
||||
</Drawer>
|
||||
</Drawer>
|
||||
</ChannelDrawer>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,38 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {loadChannelsIfNecessary, selectInitialChannel} from 'app/actions/views/channel';
|
||||
import {
|
||||
loadChannelsIfNecessary,
|
||||
loadProfilesAndTeamMembersForDMSidebar,
|
||||
selectInitialChannel
|
||||
} from 'app/actions/views/channel';
|
||||
import {openChannelDrawer} from 'app/actions/views/drawer';
|
||||
|
||||
import {getChannelsOnCurrentTeam, getCurrentChannel} from 'service/selectors/entities/channels';
|
||||
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';
|
||||
|
||||
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,
|
||||
currentTeam: getCurrentTeam(state),
|
||||
currentChannel: getCurrentChannel(state),
|
||||
channels: getChannelsOnCurrentTeam(state),
|
||||
currentChannel,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
|
@ -26,7 +44,9 @@ function mapDispatchToProps(dispatch) {
|
|||
return {
|
||||
actions: bindActionCreators({
|
||||
loadChannelsIfNecessary,
|
||||
selectInitialChannel
|
||||
loadProfilesAndTeamMembersForDMSidebar,
|
||||
selectInitialChannel,
|
||||
openChannelDrawer
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,16 +24,25 @@ export default class SelectTeam extends Component {
|
|||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.actions.fetchTeams();
|
||||
this.props.actions.websocket();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.selectFirstTeam(this.props.teams, this.props.myMembers);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.teamsRequest.status === RequestStatus.STARTED &&
|
||||
nextProps.teamsRequest.status === RequestStatus.SUCCESS) {
|
||||
const firstTeam = Object.values(nextProps.teams).sort((t) => t.name && t.name.trim().toLowerCase())[0];
|
||||
if (firstTeam) {
|
||||
this.onSelectTeam(firstTeam);
|
||||
}
|
||||
this.selectFirstTeam(nextProps.teams, nextProps.myMembers);
|
||||
}
|
||||
}
|
||||
|
||||
selectFirstTeam(allTeams, myMembers) {
|
||||
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
|
||||
const firstTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
|
||||
if (firstTeam) {
|
||||
this.onSelectTeam(firstTeam);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import * as teamActions from 'service/actions/teams';
|
||||
import {selectTeam} from 'service/actions/teams';
|
||||
import {init as websocket} from 'service/actions/websocket';
|
||||
import {goToChannelView} from 'app/actions/navigation';
|
||||
|
||||
import SelectTeamView from './select_team.js';
|
||||
|
|
@ -21,8 +22,9 @@ function mapStateToProps(state) {
|
|||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
...teamActions,
|
||||
goToChannelView
|
||||
goToChannelView,
|
||||
selectTeam,
|
||||
websocket
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
|
||||
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
|
||||
|
|
@ -31,6 +30,7 @@
|
|||
1EA2B00A1DB7E9620013B2DB /* Zocial.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 1EA2B0021DB7E9620013B2DB /* Zocial.ttf */; };
|
||||
7F55909D1DE33E62008E7FC5 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
|
||||
6D451C9F2CCA47089433C8C5 /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 054B6299E0894F978806187D /* libRNSVG.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -143,6 +143,8 @@
|
|||
1EA2B0021DB7E9620013B2DB /* Zocial.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Zocial.ttf; sourceTree = "<group>"; };
|
||||
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
|
||||
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
|
||||
EFA9957A8DD4430C85C9D4D5 /* RNSVG.xcodeproj */ = {isa = PBXFileReference; name = "RNSVG.xcodeproj"; path = "../node_modules/react-native-svg/ios/RNSVG.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
|
||||
054B6299E0894F978806187D /* libRNSVG.a */ = {isa = PBXFileReference; name = "libRNSVG.a"; path = "libRNSVG.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -168,6 +170,7 @@
|
|||
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */,
|
||||
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
|
||||
139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
|
||||
6D451C9F2CCA47089433C8C5 /* libRNSVG.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -305,6 +308,7 @@
|
|||
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */,
|
||||
00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */,
|
||||
139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */,
|
||||
EFA9957A8DD4430C85C9D4D5 /* RNSVG.xcodeproj */,
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -384,7 +388,7 @@
|
|||
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0810;
|
||||
LastUpgradeCheck = 810;
|
||||
ORGANIZATIONNAME = Facebook;
|
||||
TargetAttributes = {
|
||||
00E356ED1AD99517003FC87E = {
|
||||
|
|
@ -626,6 +630,10 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mattermost.app/Mattermost";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
);
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
|
@ -640,6 +648,10 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mattermost.app/Mattermost";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
);
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
|
@ -653,6 +665,7 @@
|
|||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../node_modules/react-native/React/**",
|
||||
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
|
||||
);
|
||||
INFOPLIST_FILE = Mattermost/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
|
|
@ -676,6 +689,7 @@
|
|||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../node_modules/react-native/React/**",
|
||||
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
|
||||
);
|
||||
INFOPLIST_FILE = Mattermost/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
|
|
@ -732,6 +746,7 @@
|
|||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../node_modules/react-native/React/**",
|
||||
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
|
|
@ -775,6 +790,7 @@
|
|||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../node_modules/react-native/React/**",
|
||||
"$(SRCROOT)/../node_modules/react-native-svg/ios/**",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"react-native-button": "1.7.1",
|
||||
"react-native-drawer": "2.3.0",
|
||||
"react-native-keyboard-spacer": "0.3.0",
|
||||
"react-native-svg": "4.3.3",
|
||||
"react-native-vector-icons": "2.1.0",
|
||||
"react-redux": "4.4.5",
|
||||
"redux": "3.6.0",
|
||||
|
|
|
|||
|
|
@ -274,6 +274,33 @@ export function fetchMyChannelsAndMembers(teamId) {
|
|||
};
|
||||
}
|
||||
|
||||
export function getMyChannelMembers(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({type: ChannelTypes.CHANNEL_MEMBERS_REQUEST}, getState);
|
||||
|
||||
let channelMembers;
|
||||
try {
|
||||
const channelMembersRequest = Client.getMyChannelMembers(teamId);
|
||||
|
||||
channelMembers = await channelMembersRequest;
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: ChannelTypes.CHANNEL_MEMBERS_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
|
||||
data: channelMembers
|
||||
},
|
||||
{
|
||||
type: ChannelTypes.CHANNEL_MEMBERS_SUCCESS
|
||||
}
|
||||
]), getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function leaveChannel(teamId, channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({type: ChannelTypes.LEAVE_CHANNEL_REQUEST}, getState);
|
||||
|
|
@ -379,11 +406,14 @@ export function viewChannel(teamId, channelId, prevChannelId = '') {
|
|||
return;
|
||||
}
|
||||
|
||||
const {channels} = getState().entities.channels;
|
||||
|
||||
const actions = [{
|
||||
type: ChannelTypes.RECEIVED_LAST_VIEWED,
|
||||
data: {
|
||||
channel_id: channelId,
|
||||
last_viewed_at: new Date().getTime()
|
||||
last_viewed_at: new Date().getTime(),
|
||||
total_msg_count: channels[channelId].total_msg_count
|
||||
}
|
||||
}];
|
||||
|
||||
|
|
@ -392,7 +422,8 @@ export function viewChannel(teamId, channelId, prevChannelId = '') {
|
|||
type: ChannelTypes.RECEIVED_LAST_VIEWED,
|
||||
data: {
|
||||
channel_id: prevChannelId,
|
||||
last_viewed_at: new Date().getTime()
|
||||
last_viewed_at: new Date().getTime(),
|
||||
total_msg_count: channels[prevChannelId].total_msg_count
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -514,6 +545,7 @@ export default {
|
|||
updateChannelNotifyProps,
|
||||
getChannel,
|
||||
fetchMyChannelsAndMembers,
|
||||
getMyChannelMembers,
|
||||
leaveChannel,
|
||||
joinChannel,
|
||||
deleteChannel,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,31 @@ import Client from 'service/client';
|
|||
import {batchActions} from 'redux-batched-actions';
|
||||
import {Constants, TeamsTypes} from 'service/constants';
|
||||
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
|
||||
import {getProfilesByIds, getStatusesByIds} from './users';
|
||||
|
||||
async function getProfilesAndStatusesForMembers(userIds, dispatch, getState) {
|
||||
const {profiles, statuses} = getState().entities.users;
|
||||
const profilesToLoad = [];
|
||||
const statusesToLoad = [];
|
||||
|
||||
userIds.forEach((userId) => {
|
||||
if (!profiles[userId]) {
|
||||
profilesToLoad.push(userId);
|
||||
}
|
||||
|
||||
if (!statuses[userId]) {
|
||||
statusesToLoad.push(userId);
|
||||
}
|
||||
});
|
||||
|
||||
if (profilesToLoad.length) {
|
||||
await getProfilesByIds(profilesToLoad)(dispatch, getState);
|
||||
}
|
||||
|
||||
if (statusesToLoad.length) {
|
||||
await getStatusesByIds(statusesToLoad)(dispatch, getState);
|
||||
}
|
||||
}
|
||||
|
||||
export function selectTeam(team) {
|
||||
return async (dispatch, getState) => {
|
||||
|
|
@ -101,6 +126,7 @@ export function getTeamMember(teamId, userId) {
|
|||
let member;
|
||||
try {
|
||||
member = await Client.getTeamMember(teamId, userId);
|
||||
getProfilesAndStatusesForMembers([userId], dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: TeamsTypes.TEAM_MEMBERS_FAILURE, error}, getState);
|
||||
|
|
@ -120,14 +146,28 @@ export function getTeamMember(teamId, userId) {
|
|||
}
|
||||
|
||||
export function getTeamMembersByIds(teamId, userIds) {
|
||||
return bindClientFunc(
|
||||
Client.getTeamMemberByIds,
|
||||
TeamsTypes.TEAM_MEMBERS_REQUEST,
|
||||
[TeamsTypes.RECEIVED_MEMBERS_IN_TEAM, TeamsTypes.TEAM_MEMBERS_SUCCESS],
|
||||
TeamsTypes.TEAM_MEMBERS_FAILURE,
|
||||
teamId,
|
||||
userIds
|
||||
);
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({type: TeamsTypes.TEAM_MEMBERS_REQUEST}, getState);
|
||||
|
||||
let members;
|
||||
try {
|
||||
members = await Client.getTeamMemberByIds(teamId, userIds);
|
||||
getProfilesAndStatusesForMembers(userIds, dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: TeamsTypes.TEAM_MEMBERS_FAILURE}, getState);
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: TeamsTypes.RECEIVED_MEMBERS_IN_TEAM,
|
||||
data: members
|
||||
},
|
||||
{
|
||||
type: TeamsTypes.TEAM_MEMBERS_SUCCESS
|
||||
}
|
||||
]), getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function getTeamStats(teamId) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import {batchActions} from 'redux-batched-actions';
|
||||
import Client from 'service/client';
|
||||
import {Constants, PreferencesTypes, UsersTypes, TeamsTypes} from 'service/constants';
|
||||
import {fetchTeams} from 'service/actions/teams';
|
||||
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
|
||||
|
||||
export function login(loginId, password, mfaToken = '') {
|
||||
|
|
@ -20,8 +21,16 @@ export function login(loginId, password, mfaToken = '') {
|
|||
|
||||
teamMembers = await teamMembersRequest;
|
||||
preferences = await preferencesRequest;
|
||||
} catch (err) {
|
||||
dispatch({type: UsersTypes.LOGIN_FAILURE, error: err}, getState);
|
||||
} catch (error) {
|
||||
dispatch({type: UsersTypes.LOGIN_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetchTeams()(dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: UsersTypes.LOGIN_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -43,13 +52,81 @@ export function login(loginId, password, mfaToken = '') {
|
|||
}
|
||||
]), getState);
|
||||
}).
|
||||
catch((err) => {
|
||||
dispatch({type: UsersTypes.LOGIN_FAILURE, error: err}, getState);
|
||||
catch((error) => {
|
||||
dispatch({type: UsersTypes.LOGIN_FAILURE, error}, getState);
|
||||
return;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadMe() {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({type: UsersTypes.LOGIN_REQUEST}, getState);
|
||||
|
||||
let user;
|
||||
dispatch({type: UsersTypes.LOGIN_REQUEST}, getState);
|
||||
try {
|
||||
user = await Client.getMe();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: UsersTypes.LOGIN_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
let preferences;
|
||||
dispatch({type: PreferencesTypes.MY_PREFERENCES_REQUEST}, getState);
|
||||
try {
|
||||
preferences = await Client.getMyPreferences();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: PreferencesTypes.MY_PREFERENCES_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetchTeams()(dispatch, getState);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: TeamsTypes.FETCH_TEAMS_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
let teamMembers;
|
||||
dispatch({type: TeamsTypes.MY_TEAM_MEMBERS_REQUEST}, getState);
|
||||
try {
|
||||
teamMembers = await Client.getMyTeamMembers();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: TeamsTypes.MY_TEAM_MEMBERS_FAILURE, error}, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: UsersTypes.RECEIVED_ME,
|
||||
data: user
|
||||
},
|
||||
{
|
||||
type: UsersTypes.LOGIN_SUCCESS
|
||||
},
|
||||
{
|
||||
type: PreferencesTypes.RECEIVED_PREFERENCES,
|
||||
data: preferences
|
||||
},
|
||||
{
|
||||
type: PreferencesTypes.MY_PREFERENCES_SUCCESS
|
||||
},
|
||||
{
|
||||
type: TeamsTypes.RECEIVED_MY_TEAM_MEMBERS,
|
||||
data: teamMembers
|
||||
},
|
||||
{
|
||||
type: TeamsTypes.MY_TEAM_MEMBERS_SUCCESS
|
||||
}
|
||||
]), getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return bindClientFunc(
|
||||
Client.logout,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
|
||||
import {getProfilesByIds, getStatusesByIds} from 'service/actions/users';
|
||||
|
||||
export function init(siteUrl, token) {
|
||||
export function init(siteUrl, token, optionalWebSocket) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({type: GeneralTypes.WEBSOCKET_REQUEST}, getState);
|
||||
const config = getState().entities.general.config;
|
||||
|
|
@ -57,7 +57,7 @@ export function init(siteUrl, token) {
|
|||
websocketClient.setEventCallback(handleEvent);
|
||||
websocketClient.setReconnectCallback(handleReconnect);
|
||||
websocketClient.setCloseCallback(handleClose);
|
||||
return websocketClient.initialize(connUrl, authToken, dispatch, getState);
|
||||
return websocketClient.initialize(connUrl, authToken, dispatch, getState, optionalWebSocket);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,16 +5,7 @@ const MAX_WEBSOCKET_FAILS = 7;
|
|||
const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
|
||||
const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
|
||||
|
||||
/* eslint-disable global-require, no-process-env */
|
||||
|
||||
let Socket;
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
Socket = require('ws');
|
||||
} else {
|
||||
Socket = WebSocket;
|
||||
}
|
||||
|
||||
/* eslint-enable global-require, no-process-env */
|
||||
|
||||
class WebSocketClient {
|
||||
constructor() {
|
||||
|
|
@ -31,7 +22,7 @@ class WebSocketClient {
|
|||
this.getState = null;
|
||||
}
|
||||
|
||||
initialize(connectionUrl = this.connectionUrl, token, dispatch, getState) {
|
||||
initialize(connectionUrl = this.connectionUrl, token, dispatch, getState, webSocketConnector = WebSocket) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.conn) {
|
||||
resolve();
|
||||
|
|
@ -54,6 +45,7 @@ class WebSocketClient {
|
|||
console.log('websocket connecting to ' + connectionUrl); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
Socket = webSocketConnector;
|
||||
this.conn = new Socket(connectionUrl);
|
||||
this.connectionUrl = connectionUrl;
|
||||
this.dispatch = dispatch;
|
||||
|
|
|
|||
|
|
@ -18,11 +18,17 @@ const Constants = {
|
|||
|
||||
DEFAULT_CHANNEL: 'town-square',
|
||||
DM_CHANNEL: 'D',
|
||||
OPEN_CHANNEL: 'O',
|
||||
PRIVATE_CHANNEL: 'P',
|
||||
|
||||
POST_DELETED: 'DELETED',
|
||||
SYSTEM_MESSAGE_PREFIX: 'system_',
|
||||
|
||||
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show'
|
||||
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
|
||||
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
|
||||
CATEGORY_FAVORITE_CHANNEL: 'favorite_channel',
|
||||
DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
|
||||
DISPLAY_PREFER_FULL_NAME: 'full_name'
|
||||
};
|
||||
|
||||
export default Constants;
|
||||
|
|
|
|||
|
|
@ -1628,14 +1628,15 @@
|
|||
"setting_upload.import": "Import",
|
||||
"setting_upload.noFile": "No file selected.",
|
||||
"setting_upload.select": "Select file",
|
||||
"sidebar.channels": "Channels",
|
||||
"sidebar.channels": "CHANNELS",
|
||||
"sidebar.createChannel": "Create new channel",
|
||||
"sidebar.createGroup": "Create new group",
|
||||
"sidebar.direct": "Direct Messages",
|
||||
"sidebar.direct": "DIRECT MESSAGES",
|
||||
"sidebar.favorites": "FAVORITES",
|
||||
"sidebar.more": "More",
|
||||
"sidebar.moreElips": "More...",
|
||||
"sidebar.otherMembers": "Outside this team",
|
||||
"sidebar.pg": "Private Groups",
|
||||
"sidebar.pg": "PRIVATE GROUPS",
|
||||
"sidebar.removeList": "Remove from list",
|
||||
"sidebar.tutorialScreen1": "<h4>Channels</h4><p><strong>Channels</strong> organize conversations across different topics. They’re open to everyone on your team. To send private communications use <strong>Direct Messages</strong> for a single person or <strong>Private Groups</strong> for multiple people.</p>",
|
||||
"sidebar.tutorialScreen2": "<h4>\"{townsquare}\" and \"{offtopic}\" channels</h4><p>Here are two public channels to start:</p><p><strong>{townsquare}</strong> is a place for team-wide communication. Everyone in your team is a member of this channel.</p><p><strong>{offtopic}</strong> is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.</p>",
|
||||
|
|
|
|||
|
|
@ -1628,14 +1628,15 @@
|
|||
"setting_upload.import": "Importar",
|
||||
"setting_upload.noFile": "No ha seleccionado un archivo",
|
||||
"setting_upload.select": "Selecciona un archivo",
|
||||
"sidebar.channels": "Canales",
|
||||
"sidebar.channels": "CANALES",
|
||||
"sidebar.createChannel": "Crear un nuevo canal",
|
||||
"sidebar.createGroup": "Crear un nuevo grupo",
|
||||
"sidebar.direct": "Mensajes Directos",
|
||||
"sidebar.direct": "MENSAJES DIRECTOS",
|
||||
"sidebar.favorites": "FAVORITOS",
|
||||
"sidebar.more": "Más",
|
||||
"sidebar.moreElips": "Más...",
|
||||
"sidebar.otherMembers": "Fuera de este equipo",
|
||||
"sidebar.pg": "Grupos Privados",
|
||||
"sidebar.pg": "GRUPOS PRIVADOS",
|
||||
"sidebar.removeList": "Remover de la lista",
|
||||
"sidebar.tutorialScreen1": "<h4>Canales</h4><p><strong>Canales</strong> organizan las conversaciones en diferentes tópicos. Son abiertos para cualquier persona de tu equipo. Para enviar comunicaciones privadas con una sola persona utiliza <strong>Mensajes Directos</strong> o con multiples personas utilizando <strong>Grupos Privados</strong>.</p>",
|
||||
"sidebar.tutorialScreen2": "<h4>Los canal \"{townsquare}\" y \"{offtopic}\"</h4><p>Estos son dos canales para comenzar:</p><p><strong>{townsquare}</strong> es el lugar para tener comunicación con todo el equipo. Todos los integrantes de tu equipo son miembros de este canal.</p><p><strong>{offtopic}</strong> es un lugar para diversión y humor fuera de los canales relacionados con el trabajo. Tu y tu equipo pueden decidir que otros canales crear.</p>",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,17 @@ function channels(state = {}, action) {
|
|||
case ChannelTypes.RECEIVED_CHANNEL_DELETED:
|
||||
Reflect.deleteProperty(nextState, action.data);
|
||||
return nextState;
|
||||
case ChannelTypes.RECEIVED_LAST_VIEWED: {
|
||||
const channelId = action.data.channel_id;
|
||||
const lastUpdatedAt = action.data.last_viewed_at;
|
||||
return {
|
||||
...state,
|
||||
[channelId]: {
|
||||
...state[channelId],
|
||||
extra_update_at: lastUpdatedAt
|
||||
}
|
||||
};
|
||||
}
|
||||
case UsersTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
|
||||
|
|
@ -72,8 +83,8 @@ function myMembers(state = {}, action) {
|
|||
case ChannelTypes.RECEIVED_LAST_VIEWED: {
|
||||
const member = {...state[action.data.channel_id]};
|
||||
member.last_viewed_at = action.data.last_viewed_at;
|
||||
member.msg_count = action.data.total_msg_count;
|
||||
member.mention_count = 0;
|
||||
member.msg_count = 0;
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
|
|
|||
|
|
@ -188,13 +188,13 @@ export default combineReducers({
|
|||
// object where every key is a user id and has an object with the users details
|
||||
profiles,
|
||||
|
||||
// object where every key is a user id and has a Set with the users id that are members of the team
|
||||
// object where every key is a team id and has a Set with the users id that are members of the team
|
||||
profilesInTeam,
|
||||
|
||||
// object where every key is a user id and has a Set with the users id that are members of the channel
|
||||
// object where every key is a channel id and has a Set with the users id that are members of the channel
|
||||
profilesInChannel,
|
||||
|
||||
// object where every key is a user id and has a Set with the users id that are members of the channel
|
||||
// object where every key is a channel id and has a Set with the users id that are members of the channel
|
||||
profilesNotInChannel,
|
||||
|
||||
// object where every key is the user id and has a value with the current status of each user
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getCurrentTeamId} from 'service/selectors/entities/teams';
|
||||
import {buildDisplayableChannelList} from 'service/utils/channel_utils';
|
||||
|
||||
function getAllChannels(state) {
|
||||
return state.entities.channels.channels;
|
||||
|
|
@ -28,7 +28,7 @@ export const getChannelsOnCurrentTeam = createSelector(
|
|||
const channels = [];
|
||||
|
||||
for (const channel of Object.values(allChannels)) {
|
||||
if (channel.team_id === currentTeamId) {
|
||||
if (channel.team_id === currentTeamId || channel.team_id === '') {
|
||||
channels.push(channel);
|
||||
}
|
||||
}
|
||||
|
|
@ -36,3 +36,13 @@ export const getChannelsOnCurrentTeam = createSelector(
|
|||
return channels;
|
||||
}
|
||||
);
|
||||
|
||||
export const getChannelsByCategory = createSelector(
|
||||
getChannelsOnCurrentTeam,
|
||||
(state) => state.entities.users,
|
||||
(state) => state.entities.preferences.myPreferences,
|
||||
(state) => state.entities.teams,
|
||||
(channels, usersState, myPreferences, teamsState) => {
|
||||
return buildDisplayableChannelList(usersState, teamsState, channels, myPreferences);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
170
service/utils/channel_utils.js
Normal file
170
service/utils/channel_utils.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Constants} from 'service/constants';
|
||||
import {displayUsername} from './user_utils';
|
||||
import {getPreferencesByCategory} from './preference_utils';
|
||||
|
||||
const defaultPrefix = 'D'; // fallback for future types
|
||||
const typeToPrefixMap = {[Constants.OPEN_CHANNEL]: 'A', [Constants.PRIVATE_CHANNEL]: 'B', [Constants.DM_CHANNEL]: 'C'};
|
||||
|
||||
export function buildDisplayableChannelList(usersState, teamsState, allChannels, myPreferences) {
|
||||
const missingDMChannels = createMissingDirectChannels(usersState.currentId, allChannels, myPreferences);
|
||||
const channels = allChannels.
|
||||
concat(missingDMChannels).
|
||||
map(completeDirectChannelInfo.bind(null, usersState, myPreferences));
|
||||
|
||||
channels.sort((a, b) => {
|
||||
const locale = usersState.profiles[usersState.currentId].locale;
|
||||
|
||||
return buildDisplayNameAndTypeComparable(a).
|
||||
localeCompare(buildDisplayNameAndTypeComparable(b), locale, {numeric: true});
|
||||
});
|
||||
|
||||
const favoriteChannels = channels.filter(isFavoriteChannel.bind(null, myPreferences));
|
||||
const notFavoriteChannels = channels.filter(not(isFavoriteChannel.bind(null, myPreferences)));
|
||||
const directChannels = notFavoriteChannels.
|
||||
filter(
|
||||
andX(
|
||||
isDirectChannel,
|
||||
isDirectChannelVisible.bind(null, usersState.currentId, myPreferences)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
favoriteChannels,
|
||||
publicChannels: notFavoriteChannels.filter(isOpenChannel),
|
||||
privateChannels: notFavoriteChannels.filter(isPrivateChannel),
|
||||
directChannels: directChannels.filter(
|
||||
isConnectedToTeamMember.bind(null, teamsState.membersInTeam[teamsState.currentId])
|
||||
),
|
||||
directNonTeamChannels: directChannels.filter(
|
||||
isNotConnectedToTeamMember.bind(null, teamsState.membersInTeam[teamsState.currentId])
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export function getDirectChannelName(id, otherId) {
|
||||
let handle;
|
||||
|
||||
if (otherId > id) {
|
||||
handle = id + '__' + otherId;
|
||||
} else {
|
||||
handle = otherId + '__' + id;
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
export function getChannelByName(channels, name) {
|
||||
const channelIds = Object.keys(channels);
|
||||
for (let i = 0; i < channelIds.length; i++) {
|
||||
const id = channelIds[i];
|
||||
if (channels[id].name === name) {
|
||||
return channels[id];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isOpenChannel(channel) {
|
||||
return channel.type === Constants.OPEN_CHANNEL;
|
||||
}
|
||||
|
||||
function isPrivateChannel(channel) {
|
||||
return channel.type === Constants.PRIVATE_CHANNEL;
|
||||
}
|
||||
|
||||
function isConnectedToTeamMember(members, channel) {
|
||||
return members && members.has(channel.teammate_id);
|
||||
}
|
||||
|
||||
function isNotConnectedToTeamMember(members, channel) {
|
||||
if (!members) {
|
||||
return true;
|
||||
}
|
||||
return !members.has(channel.teammate_id);
|
||||
}
|
||||
|
||||
function isDirectChannel(channel) {
|
||||
return channel.type === Constants.DM_CHANNEL;
|
||||
}
|
||||
|
||||
function isDirectChannelVisible(userId, myPreferences, channel) {
|
||||
const channelId = getUserIdFromChannelName(userId, channel);
|
||||
const dm = myPreferences[`${Constants.CATEGORY_DIRECT_CHANNEL_SHOW}--${channelId}`];
|
||||
return dm && dm.value === 'true';
|
||||
}
|
||||
|
||||
function isFavoriteChannel(myPreferences, channel) {
|
||||
const fav = myPreferences[`${Constants.CATEGORY_FAVORITE_CHANNEL}--${channel.id}`];
|
||||
return fav && fav.value === 'true';
|
||||
}
|
||||
|
||||
function createMissingDirectChannels(currentUserId, allChannels, myPreferences) {
|
||||
const preferences = getPreferencesByCategory(myPreferences, Constants.CATEGORY_DIRECT_CHANNEL_SHOW);
|
||||
|
||||
return Array.
|
||||
from(preferences).
|
||||
filter((entry) => entry[1] === 'true').
|
||||
map((entry) => entry[0]).
|
||||
filter((teammateId) => !allChannels.some(isDirectChannelForUser.bind(null, currentUserId, teammateId))).
|
||||
map(createFakeChannelCurried(currentUserId));
|
||||
}
|
||||
|
||||
function isDirectChannelForUser(userId, otherUserId, channel) {
|
||||
return channel.type === Constants.DM_CHANNEL && getUserIdFromChannelName(userId, channel) === otherUserId;
|
||||
}
|
||||
|
||||
export function getUserIdFromChannelName(userId, channel) {
|
||||
const ids = channel.name.split('__');
|
||||
let otherUserId = '';
|
||||
if (ids[0] === userId) {
|
||||
otherUserId = ids[1];
|
||||
} else {
|
||||
otherUserId = ids[0];
|
||||
}
|
||||
|
||||
return otherUserId;
|
||||
}
|
||||
|
||||
function createFakeChannel(userId, otherUserId) {
|
||||
return {
|
||||
name: getDirectChannelName(userId, otherUserId),
|
||||
last_post_at: 0,
|
||||
total_msg_count: 0,
|
||||
type: Constants.DM_CHANNEL,
|
||||
fake: true
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeChannelCurried(userId) {
|
||||
return (otherUserId) => createFakeChannel(userId, otherUserId);
|
||||
}
|
||||
|
||||
function completeDirectChannelInfo(usersState, myPreferences, channel) {
|
||||
if (!isDirectChannel(channel)) {
|
||||
return channel;
|
||||
}
|
||||
|
||||
const dmChannelClone = JSON.parse(JSON.stringify(channel));
|
||||
const teammateId = getUserIdFromChannelName(usersState.currentId, channel);
|
||||
|
||||
return Object.assign(dmChannelClone, {
|
||||
display_name: displayUsername(usersState.profiles[teammateId], myPreferences),
|
||||
teammate_id: teammateId,
|
||||
status: usersState.statuses[teammateId] || 'offline'
|
||||
});
|
||||
}
|
||||
|
||||
function buildDisplayNameAndTypeComparable(channel) {
|
||||
return (typeToPrefixMap[channel.type] || defaultPrefix) + channel.display_name + channel.name;
|
||||
}
|
||||
|
||||
function not(f) {
|
||||
return (...args) => !f(...args);
|
||||
}
|
||||
|
||||
function andX(...fns) {
|
||||
return (...args) => fns.every((f) => f(...args));
|
||||
}
|
||||
14
service/utils/preference_utils.js
Normal file
14
service/utils/preference_utils.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
export function getPreferencesByCategory(myPreferences, category) {
|
||||
const prefix = `${category}--`;
|
||||
const preferences = new Map();
|
||||
Object.keys(myPreferences).forEach((key) => {
|
||||
if (key.startsWith(prefix)) {
|
||||
preferences.set(key.substring(prefix.length), myPreferences[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return preferences;
|
||||
}
|
||||
38
service/utils/user_utils.js
Normal file
38
service/utils/user_utils.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Constants} from 'service/constants';
|
||||
|
||||
export function getFullName(user) {
|
||||
if (user.first_name && user.last_name) {
|
||||
return user.first_name + ' ' + user.last_name;
|
||||
} else if (user.first_name) {
|
||||
return user.first_name;
|
||||
} else if (user.last_name) {
|
||||
return user.last_name;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function displayUsername(user, myPreferences) {
|
||||
let nameFormat = 'false';
|
||||
const pref = myPreferences[`${Constants.CATEGORY_DISPLAY_SETTINGS}--name_format`];
|
||||
if (pref && pref.value) {
|
||||
nameFormat = pref.value;
|
||||
}
|
||||
let username = '';
|
||||
|
||||
if (user) {
|
||||
if (nameFormat === Constants.DISPLAY_PREFER_NICKNAME) {
|
||||
username = user.nickname || getFullName(user);
|
||||
} else if (nameFormat === Constants.DISPLAY_PREFER_FULL_NAME) {
|
||||
username = getFullName(user);
|
||||
}
|
||||
|
||||
if (!username.trim().length) {
|
||||
username = user.username;
|
||||
}
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
import 'react-native';
|
||||
|
||||
global.WebSocket = require('ws');
|
||||
|
||||
// Set up a global hooks to make debugging tests less of a pain
|
||||
before(() => {
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ describe('Actions.Channels', () => {
|
|||
};
|
||||
|
||||
await Actions.createChannel(channel, TestHelper.basicUser.id)(store.dispatch, store.getState);
|
||||
const {createChannel: createRequest, myMembers: membersRequest} = store.getState().requests.channels;
|
||||
const createRequest = store.getState().requests.channels.createChannel;
|
||||
const membersRequest = store.getState().requests.channels.myMembers;
|
||||
if (createRequest.status === RequestStatus.FAILURE) {
|
||||
throw new Error(JSON.stringify(createRequest.error));
|
||||
} else if (membersRequest.status === RequestStatus.FAILURE) {
|
||||
|
|
@ -81,19 +82,19 @@ describe('Actions.Channels', () => {
|
|||
}
|
||||
|
||||
const state = store.getState();
|
||||
const {channels, myMembers: members} = state.entities.channels;
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const profiles = state.entities.users.profiles;
|
||||
const preferences = state.entities.preferences.myPreferences;
|
||||
const channelsCount = Object.keys(channels).length;
|
||||
const membersCount = Object.keys(members).length;
|
||||
const membersCount = Object.keys(myMembers).length;
|
||||
|
||||
assert.ok(channels, 'channels is empty');
|
||||
assert.ok(members, 'members is empty');
|
||||
assert.ok(myMembers, 'members is empty');
|
||||
assert.ok(profiles[user.id], 'profiles does not have userId');
|
||||
assert.ok(Object.keys(preferences).length, 'preferences is empty');
|
||||
assert.ok(channels[Object.keys(members)[0]], 'channels should have the member');
|
||||
assert.ok(members[Object.keys(channels)[0]], 'members should belong to channel');
|
||||
assert.equal(members[Object.keys(channels)[0]].user_id, TestHelper.basicUser.id);
|
||||
assert.ok(channels[Object.keys(myMembers)[0]], 'channels should have the member');
|
||||
assert.ok(myMembers[Object.keys(channels)[0]], 'members should belong to channel');
|
||||
assert.equal(myMembers[Object.keys(channels)[0]].user_id, TestHelper.basicUser.id);
|
||||
assert.equal(channelsCount, membersCount);
|
||||
assert.equal(channels[Object.keys(channels)[0]].type, 'D');
|
||||
assert.equal(channelsCount, 1);
|
||||
|
|
@ -137,7 +138,8 @@ describe('Actions.Channels', () => {
|
|||
it('fetchMyChannelsAndMembers', async () => {
|
||||
await Actions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)(store.dispatch, store.getState);
|
||||
|
||||
const {getChannels: channelsRequest, myMembers: membersRequest} = store.getState().requests.channels;
|
||||
const channelsRequest = store.getState().requests.channels.getChannels;
|
||||
const membersRequest = store.getState().requests.channels.myMembers;
|
||||
if (channelsRequest.status === RequestStatus.FAILURE) {
|
||||
throw new Error(JSON.stringify(channelsRequest.error));
|
||||
} else if (membersRequest.status === RequestStatus.FAILURE) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import * as Actions from 'service/actions/websocket';
|
||||
import * as ChannelActions from 'service/actions/channels';
|
||||
|
|
@ -15,7 +16,12 @@ describe('Actions.Websocket', () => {
|
|||
before(async () => {
|
||||
store = configureStore();
|
||||
await TestHelper.initBasic(Client);
|
||||
return await Actions.init()(store.dispatch, store.getState);
|
||||
const webSocketConnector = require('ws');
|
||||
return await Actions.init(
|
||||
null,
|
||||
null,
|
||||
webSocketConnector
|
||||
)(store.dispatch, store.getState);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue