Header redesign (#133)

* Search

* Custom search on iOS and Android
This commit is contained in:
Diego Mello 2017-12-08 17:13:21 -02:00 committed by Guilherme Gazzo
parent d991fd99bc
commit c84c5a3565
23 changed files with 831 additions and 389 deletions

View File

@ -35,15 +35,16 @@ exports[`render channel 1`] = `
Array [ Array [
Object { Object {
"alignItems": "center", "alignItems": "center",
"borderRadius": 4,
"height": 40,
"justifyContent": "center", "justifyContent": "center",
"overflow": "hidden", "overflow": "hidden",
"width": 40,
}, },
Object { Object {
"backgroundColor": "#00BCD4", "backgroundColor": "#00BCD4",
"borderRadius": 4,
"height": 40,
"width": 40,
}, },
undefined,
] ]
} }
> >
@ -57,10 +58,14 @@ exports[`render channel 1`] = `
"color": undefined, "color": undefined,
"fontSize": 12, "fontSize": 12,
}, },
Object { Array [
"color": "#fff", Object {
"fontSize": 20, "color": "#ffffff",
}, },
Object {
"fontSize": 20,
},
],
Object { Object {
"fontFamily": "Material Design Icons", "fontFamily": "Material Design Icons",
"fontStyle": "normal", "fontStyle": "normal",
@ -151,6 +156,53 @@ exports[`render no icon 1`] = `
testID={undefined} testID={undefined}
tvParallaxProperties={undefined} tvParallaxProperties={undefined}
> >
<View
style={
Array [
Object {
"alignItems": "center",
"justifyContent": "center",
"overflow": "hidden",
},
Object {
"backgroundColor": "#3F51B5",
"borderRadius": 4,
"height": 40,
"width": 40,
},
undefined,
]
}
>
<Text
accessible={true}
allowFontScaling={false}
ellipsizeMode="tail"
style={
Array [
Object {
"color": undefined,
"fontSize": 12,
},
Array [
Object {
"color": "#ffffff",
},
Object {
"fontSize": 20,
},
],
Object {
"fontFamily": "Material Design Icons",
"fontStyle": "normal",
"fontWeight": "normal",
},
]
}
>
</Text>
</View>
<View <View
style={ style={
Object { Object {
@ -230,6 +282,53 @@ exports[`render private group 1`] = `
testID={undefined} testID={undefined}
tvParallaxProperties={undefined} tvParallaxProperties={undefined}
> >
<View
style={
Array [
Object {
"alignItems": "center",
"justifyContent": "center",
"overflow": "hidden",
},
Object {
"backgroundColor": "#FF9800",
"borderRadius": 4,
"height": 40,
"width": 40,
},
undefined,
]
}
>
<Text
accessible={true}
allowFontScaling={false}
ellipsizeMode="tail"
style={
Array [
Object {
"color": undefined,
"fontSize": 12,
},
Array [
Object {
"color": "#ffffff",
},
Object {
"fontSize": 20,
},
],
Object {
"fontFamily": "Material Design Icons",
"fontStyle": "normal",
"fontWeight": "normal",
},
]
}
>
</Text>
</View>
<View <View
style={ style={
Object { Object {

View File

@ -27,7 +27,7 @@ export const FORGOT_PASSWORD = createRequestTypes('FORGOT_PASSWORD', [
'INIT' 'INIT'
]); ]);
export const USER = createRequestTypes('USER', ['SET']); export const USER = createRequestTypes('USER', ['SET']);
export const ROOMS = createRequestTypes('ROOMS'); export const ROOMS = createRequestTypes('ROOMS', [...defaultTypes, 'SET_SEARCH']);
export const ROOM = createRequestTypes('ROOM', ['ADD_USER_TYPING', 'REMOVE_USER_TYPING', 'SOMEONE_TYPING', 'OPEN', 'USER_TYPING']); export const ROOM = createRequestTypes('ROOM', ['ADD_USER_TYPING', 'REMOVE_USER_TYPING', 'SOMEONE_TYPING', 'OPEN', 'USER_TYPING']);
export const APP = createRequestTypes('APP', ['READY', 'INIT']); export const APP = createRequestTypes('APP', ['READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES', [ export const MESSAGES = createRequestTypes('MESSAGES', [
@ -75,6 +75,7 @@ export const SERVER = createRequestTypes('SERVER', [
]); ]);
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']); export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']);
export const LOGOUT = 'LOGOUT'; // logout is always success export const LOGOUT = 'LOGOUT'; // logout is always success
export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET', 'REQUEST']);
export const INCREMENT = 'INCREMENT'; export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT'; export const DECREMENT = 'DECREMENT';

View File

@ -0,0 +1,15 @@
import * as types from './actionsTypes';
export function requestActiveUser(user) {
return {
type: types.ACTIVE_USERS.REQUEST,
user
};
}
export function setActiveUser(data) {
return {
type: types.ACTIVE_USERS.SET,
data
};
}

View File

@ -19,3 +19,10 @@ export function roomsFailure(err) {
err err
}; };
} }
export function setSearch(searchText) {
return {
type: types.ROOMS.SET_SEARCH,
searchText
};
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { CachedImage } from 'react-native-img-cache'; import { CachedImage } from 'react-native-img-cache';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor'; import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -21,7 +22,7 @@ const styles = StyleSheet.create({
class Avatar extends React.PureComponent { class Avatar extends React.PureComponent {
render() { render() {
const { const {
text = '', size = 25, baseUrl, borderRadius = 4, style, avatar text = '', size = 25, baseUrl, borderRadius = 4, style, avatar, type = 'd'
} = this.props; } = this.props;
const { initials, color } = avatarInitialsAndColor(`${ text }`); const { initials, color } = avatarInitialsAndColor(`${ text }`);
@ -42,19 +43,32 @@ class Avatar extends React.PureComponent {
borderRadius borderRadius
}; };
const uri = avatar || `${ baseUrl }/avatar/${ text }`; if (type === 'd') {
const image = (avatar || baseUrl) && ( const uri = avatar || `${ baseUrl }/avatar/${ text }`;
<CachedImage const image = (avatar || baseUrl) && (
style={[styles.avatar, avatarStyle]} <CachedImage
source={{ uri }} style={[styles.avatar, avatarStyle]}
/> source={{ uri }}
); />
);
return (
<View style={[styles.iconContainer, iconContainerStyle, style]}>
<Text style={[styles.avatarInitials, avatarInitialsStyle]}>{initials}</Text>
{image}
</View>);
}
const icon = {
c: 'pound',
p: 'lock',
l: 'account'
}[type];
return ( return (
<View style={[styles.iconContainer, iconContainerStyle, style]}> <View style={[styles.iconContainer, iconContainerStyle, style]}>
<Text style={[styles.avatarInitials, avatarInitialsStyle]}>{initials}</Text> <MaterialCommunityIcons name={icon} style={[styles.avatarInitials, avatarInitialsStyle]} />
{image} </View>
</View>); );
} }
} }
@ -64,6 +78,7 @@ Avatar.propTypes = {
text: PropTypes.string.isRequired, text: PropTypes.string.isRequired,
avatar: PropTypes.string, avatar: PropTypes.string,
size: PropTypes.number, size: PropTypes.number,
borderRadius: PropTypes.number borderRadius: PropTypes.number,
type: PropTypes.string
}; };
export default Avatar; export default Avatar;

View File

@ -1,18 +1,8 @@
import React from 'react'; import React from 'react';
import { Text, View, StyleSheet, Platform, TouchableOpacity, Dimensions } from 'react-native'; import { View, StyleSheet, Platform } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Modal from 'react-native-modal';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import DrawerMenuButton from '../presentation/DrawerMenuButton';
import Avatar from './Avatar';
import RocketChat from '../lib/rocketchat';
import { STATUS_COLORS } from '../constants/colors';
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
let platformContainerStyles; let platformContainerStyles;
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
platformContainerStyles = { platformContainerStyles = {
@ -32,7 +22,6 @@ if (Platform.OS === 'ios') {
} }
const appBarHeight = Platform.OS === 'ios' ? 44 : 56; const appBarHeight = Platform.OS === 'ios' ? 44 : 56;
const { width } = Dimensions.get('window');
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF', backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
@ -41,171 +30,20 @@ const styles = StyleSheet.create({
}, },
appBar: { appBar: {
flex: 1 flex: 1
},
header: {
flexDirection: 'row',
alignItems: 'center',
flex: 1
},
titleContainer: {
left: TITLE_OFFSET,
right: TITLE_OFFSET,
position: 'absolute',
alignItems: 'center',
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
flexDirection: 'row'
},
status: {
borderRadius: 4,
width: 8,
height: 8,
marginRight: 10
},
avatar: {
marginRight: 10
},
title: {
fontWeight: 'bold'
},
left: {
left: 0,
position: 'absolute'
},
right: {
right: 0,
position: 'absolute'
},
modal: {
width: width - 60,
height: width - 60,
backgroundColor: '#F7F7F7',
borderRadius: 4,
flexDirection: 'column'
},
modalButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0, 0, 0, .3)',
paddingHorizontal: 20
} }
}); });
@connect(state => ({
user: state.login.user,
Site_Url: state.settings.Site_Url
}))
export default class extends React.PureComponent { export default class extends React.PureComponent {
static propTypes = { static propTypes = {
navigation: PropTypes.object.isRequired, subview: PropTypes.object.isRequired
user: PropTypes.object.isRequired,
Site_Url: PropTypes.string
} }
constructor(props) {
super(props);
this.state = {
isModalVisible: false
};
}
onPressModalButton(status) {
RocketChat.setUserPresenceDefaultStatus(status);
this.hideModal();
}
showModal() {
this.setState({ isModalVisible: true });
}
hideModal() {
this.setState({ isModalVisible: false });
}
createChannel() {
this.props.navigation.navigate('SelectUsers');
}
renderTitle() {
if (!this.props.user.username) {
return null;
}
return (
<TouchableOpacity style={styles.titleContainer} onPress={() => this.showModal()}>
<View style={[styles.status, { backgroundColor: STATUS_COLORS[this.props.user.status || 'offline'] }]} />
<Avatar
text={this.props.user.username}
size={24}
style={{ marginRight: 5 }}
baseUrl={this.props.Site_Url}
/>
<Text style={styles.title}>{this.props.user.username}</Text>
</TouchableOpacity>
);
}
renderRight() {
if (Platform.OS !== 'ios') {
return;
}
return (
<View style={styles.right}>
<Icon.Button
name='ios-create-outline'
color='blue'
size={26}
backgroundColor='transparent'
onPress={() => this.createChannel()}
/>
</View>
);
}
renderModalButton = (status, text) => {
const statusStyle = [styles.status, { backgroundColor: STATUS_COLORS[status] }];
const textStyle = { flex: 1, fontWeight: this.props.user.status === status ? 'bold' : 'normal' };
return (
<TouchableOpacity
style={styles.modalButton}
onPress={() => this.onPressModalButton(status)}
>
<View style={statusStyle} />
<Text style={textStyle}>
{text || status.charAt(0).toUpperCase() + status.slice(1)}
</Text>
</TouchableOpacity>
);
};
render() { render() {
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
<View style={styles.appBar}> <View style={styles.appBar}>
<View style={styles.header}> {this.props.subview}
<View style={styles.left}>
<DrawerMenuButton navigation={this.props.navigation} />
</View>
{this.renderTitle()}
{this.renderRight()}
</View>
</View> </View>
<Modal
isVisible={this.state.isModalVisible}
supportedOrientations={['portrait', 'landscape']}
style={{ alignItems: 'center' }}
onModalHide={() => this.hideModal()}
onBackdropPress={() => this.hideModal()}
>
<View style={styles.modal}>
{this.renderModalButton('online')}
{this.renderModalButton('busy')}
{this.renderModalButton('away')}
{this.renderModalButton('offline', 'Invisible')}
</View>
</Modal>
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@ -1,38 +1,19 @@
import React from 'react';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { StackNavigator, DrawerNavigator } from 'react-navigation'; import { StackNavigator, DrawerNavigator } from 'react-navigation';
import Sidebar from '../../containers/Sidebar'; import Sidebar from '../../containers/Sidebar';
import DrawerMenuButton from '../../presentation/DrawerMenuButton';
import Header from '../../containers/Header';
import RoomsListView from '../../views/RoomsListView'; import RoomsListView from '../../views/RoomsListView';
import RoomView from '../../views/RoomView'; import RoomView from '../../views/RoomView';
import CreateChannelView from '../../views/CreateChannelView'; import CreateChannelView from '../../views/CreateChannelView';
import SelectUsersView from '../../views/SelectUsersView'; import SelectUsersView from '../../views/SelectUsersView';
const drawerPosition = 'left';
const drawerIconPosition = 'headerLeft';
const AuthRoutes = StackNavigator( const AuthRoutes = StackNavigator(
{ {
RoomsList: { RoomsList: {
screen: RoomsListView, screen: RoomsListView
navigationOptions({ navigation }) {
return {
title: 'Rooms',
header: <Header navigation={navigation} />
};
}
}, },
Room: { Room: {
screen: RoomView, screen: RoomView
navigationOptions({ navigation }) {
return {
title: navigation.state.params.name || navigation.state.params.room.name || 'Room'
// [drawerIconPosition]: (<DrawerMenuButton navigation={navigation} />)÷
};
}
}, },
CreateChannel: { CreateChannel: {
screen: CreateChannelView, screen: CreateChannelView,
@ -53,18 +34,11 @@ const AuthRoutes = StackNavigator(
const Routes = DrawerNavigator( const Routes = DrawerNavigator(
{ {
Home: { Home: {
screen: AuthRoutes, screen: AuthRoutes
navigationOptions({ navigation }) {
return {
title: 'Rooms',
[drawerIconPosition]: <DrawerMenuButton navigation={navigation} />
};
}
} }
}, },
{ {
contentComponent: Sidebar, contentComponent: Sidebar,
drawerPosition,
navigationOptions: { navigationOptions: {
drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked' drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked'
} }

View File

@ -11,6 +11,7 @@ import * as actions from '../actions';
import { someoneTyping } from '../actions/room'; import { someoneTyping } from '../actions/room';
import { setUser } from '../actions/login'; import { setUser } from '../actions/login';
import { disconnect, connectSuccess } from '../actions/connect'; import { disconnect, connectSuccess } from '../actions/connect';
import { requestActiveUser } from '../actions/activeUsers';
export { Accounts } from 'react-native-meteor'; export { Accounts } from 'react-native-meteor';
@ -47,6 +48,23 @@ const RocketChat = {
} }
throw new Error({ error: 'invalid server' }); throw new Error({ error: 'invalid server' });
}, },
_setUser(ddpMessage) {
let status;
if (!ddpMessage.fields) {
status = 'offline';
} else {
status = ddpMessage.fields.status || 'offline';
}
const { user } = reduxStore.getState().login;
if (user && user.id === ddpMessage.id) {
return reduxStore.dispatch(setUser({ status }));
}
const activeUser = {};
activeUser[ddpMessage.id] = status;
return reduxStore.dispatch(requestActiveUser(activeUser));
},
connect(_url) { connect(_url) {
return new Promise((resolve) => { return new Promise((resolve) => {
const url = `${ _url }/websocket`; const url = `${ _url }/websocket`;
@ -63,6 +81,16 @@ const RocketChat = {
}); });
Meteor.ddp.on('connected', async() => { Meteor.ddp.on('connected', async() => {
Meteor.ddp.on('added', (ddpMessage) => {
if (ddpMessage.collection === 'users') {
return RocketChat._setUser(ddpMessage);
}
});
Meteor.ddp.on('removed', (ddpMessage) => {
if (ddpMessage.collection === 'users') {
return RocketChat._setUser(ddpMessage);
}
});
Meteor.ddp.on('changed', (ddpMessage) => { Meteor.ddp.on('changed', (ddpMessage) => {
if (ddpMessage.collection === 'stream-room-messages') { if (ddpMessage.collection === 'stream-room-messages') {
return realm.write(() => { return realm.write(() => {
@ -96,7 +124,7 @@ const RocketChat = {
} }
} }
if (ddpMessage.collection === 'users') { if (ddpMessage.collection === 'users') {
return reduxStore.dispatch(setUser({ status: ddpMessage.fields.status || ddpMessage.fields.statusDefault })); return RocketChat._setUser(ddpMessage);
} }
}); });
RocketChat.getSettings(); RocketChat.getSettings();
@ -436,7 +464,7 @@ const RocketChat = {
}); });
Meteor.subscribe('stream-notify-user', `${ login.user.id }/subscriptions-changed`, false); Meteor.subscribe('stream-notify-user', `${ login.user.id }/subscriptions-changed`, false);
Meteor.subscribe('stream-notify-user', `${ login.user.id }/rooms-changed`, false); Meteor.subscribe('stream-notify-user', `${ login.user.id }/rooms-changed`, false);
Meteor.subscribe('userData', null, false); Meteor.subscribe('activeUsers', null, false);
return data; return data;
}, },
logout({ server }) { logout({ server }) {

View File

@ -1,18 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
const DrawerMenuButton = ({ navigation }) => (
<TouchableOpacity
onPress={() => navigation.navigate('DrawerOpen')}
>
<Icon name='bars' style={{ padding: 10, marginLeft: 10 }} size={20} color='black' />
</TouchableOpacity>
);
DrawerMenuButton.propTypes = {
navigation: PropTypes.object.isRequired
};
export default DrawerMenuButton;

View File

@ -1,10 +1,8 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Avatar from '../containers/Avatar'; import Avatar from '../containers/Avatar';
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -49,22 +47,6 @@ const styles = StyleSheet.create({
fontSize: 10, fontSize: 10,
height: 10, height: 10,
color: '#888' color: '#888'
},
iconContainer: {
height: 40,
width: 40,
borderRadius: 4,
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center'
},
icon: {
fontSize: 20,
color: '#fff'
},
avatarInitials: {
fontSize: 20,
color: '#ffffff'
} }
}); });
@ -83,30 +65,7 @@ export default class RoomItem extends React.PureComponent {
get icon() { get icon() {
const { type, name, baseUrl } = this.props; const { type, name, baseUrl } = this.props;
return <Avatar text={name} baseUrl={baseUrl} size={40} type={type} />;
const icon = {
d: 'at',
c: 'pound',
p: 'lock',
l: 'account'
}[type];
if (!icon) {
return null;
}
if (type === 'd') {
return (
<Avatar text={name} baseUrl={baseUrl} size={40} />
);
}
const { color } = avatarInitialsAndColor(name);
return (
<View style={[styles.iconContainer, { backgroundColor: color }]}>
<MaterialCommunityIcons name={icon} style={styles.icon} />
</View>
);
} }
formatDate = date => moment(date).calendar(null, { formatDate = date => moment(date).calendar(null, {

View File

@ -0,0 +1,15 @@
import * as types from '../actions/actionsTypes';
const initialState = {};
export default (state = initialState, action) => {
switch (action.type) {
case types.ACTIVE_USERS.SET:
return {
...state,
...action.data
};
default:
return state;
}
};

View File

@ -4,12 +4,14 @@ import login from './login';
import meteor from './connect'; import meteor from './connect';
import messages from './messages'; import messages from './messages';
import room from './room'; import room from './room';
import rooms from './rooms';
import server from './server'; import server from './server';
import navigator from './navigator'; import navigator from './navigator';
import createChannel from './createChannel'; import createChannel from './createChannel';
import app from './app'; import app from './app';
import permissions from './permissions'; import permissions from './permissions';
import activeUsers from './activeUsers';
export default combineReducers({ export default combineReducers({
settings, login, meteor, messages, server, navigator, createChannel, app, room, permissions settings, login, meteor, messages, server, navigator, createChannel, app, room, rooms, permissions, activeUsers
}); });

View File

@ -2,7 +2,8 @@ import * as types from '../actions/actionsTypes';
const initialState = { const initialState = {
isFetching: false, isFetching: false,
failure: false failure: false,
searchText: ''
}; };
export default function login(state = initialState, action) { export default function login(state = initialState, action) {
@ -24,8 +25,11 @@ export default function login(state = initialState, action) {
failure: true, failure: true,
errorMessage: action.err errorMessage: action.err
}; };
// case types.LOGOUT: case types.ROOMS.SET_SEARCH:
// return initialState; return {
...state,
searchText: action.searchText
};
default: default:
return state; return state;
} }

30
app/sagas/activeUsers.js Normal file
View File

@ -0,0 +1,30 @@
import { put, take, race, fork } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import * as types from '../actions/actionsTypes';
import { setActiveUser } from '../actions/activeUsers';
const watchActiveUsers = function* handleInput() {
let obj = {};
while (true) {
const { status, timeout } = yield race({
status: take(types.ACTIVE_USERS.REQUEST),
timeout: delay(3000)
});
if (timeout && Object.keys(obj).length > 0) {
yield put(setActiveUser(obj));
obj = {};
}
if (status) {
obj = {
...obj,
...status.user
};
}
}
};
const root = function* root() {
yield fork(watchActiveUsers);
};
export default root;

View File

@ -8,6 +8,7 @@ import selectServer from './selectServer';
import createChannel from './createChannel'; import createChannel from './createChannel';
import init from './init'; import init from './init';
import state from './state'; import state from './state';
import activeUsers from './activeUsers';
const root = function* root() { const root = function* root() {
yield all([ yield all([
@ -19,7 +20,8 @@ const root = function* root() {
connect(), connect(),
messages(), messages(),
selectServer(), selectServer(),
state() state(),
activeUsers()
]); ]);
}; };

View File

@ -0,0 +1,115 @@
import React from 'react';
import { Text, View, Platform, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { HeaderBackButton } from 'react-navigation';
import realm from '../../../lib/realm';
import Avatar from '../../../containers/Avatar';
import { STATUS_COLORS } from '../../../constants/colors';
import styles from './styles';
@connect(state => ({
user: state.login.user,
baseUrl: state.settings.Site_Url,
activeUsers: state.activeUsers
}))
export default class extends React.Component {
static propTypes = {
navigation: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
baseUrl: PropTypes.string,
activeUsers: PropTypes.object
}
constructor(props) {
super(props);
this.state = {
room: {},
roomName: props.navigation.state.params.name
};
this.rid = props.navigation.state.params.room.rid;
this.room = realm.objects('subscriptions').filtered('rid = $0', this.rid);
this.room.addListener(this.updateState);
}
componentDidMount() {
this.updateState();
}
componentWillUnmount() {
this.room.removeAllListeners();
}
getUserStatus() {
const userId = this.rid.replace(this.props.user.id, '').trim();
return this.props.activeUsers[userId] || 'offline';
}
getUserStatusLabel() {
const status = this.getUserStatus();
return status.charAt(0).toUpperCase() + status.slice(1);
}
updateState = () => {
this.setState({ room: this.room[0] });
};
isDirect = () => this.state.room && this.state.room.t === 'd';
renderLeft = () => <HeaderBackButton onPress={() => this.props.navigation.goBack(null)} tintColor='#292E35' />;
renderTitle() {
if (!this.state.roomName) {
return null;
}
return (
<TouchableOpacity style={styles.titleContainer}>
{this.isDirect() ?
<View style={[styles.status, { backgroundColor: STATUS_COLORS[this.getUserStatus()] }]} />
: null
}
<Avatar
text={this.state.roomName}
size={24}
style={{ marginRight: 5 }}
baseUrl={this.props.baseUrl}
type={this.state.room.t}
/>
<View style={{ flexDirection: 'column' }}>
<Text style={styles.title}>{this.state.roomName}</Text>
{this.isDirect() ?
<Text style={styles.userStatus}>{this.getUserStatusLabel()}</Text>
: null
}
</View>
</TouchableOpacity>
);
}
renderRight = () => (
<View style={styles.right}>
<TouchableOpacity
style={styles.headerButton}
onPress={() => {}}
>
<Icon
name={Platform.OS === 'ios' ? 'ios-more' : 'md-more'}
color='#292E35'
size={24}
backgroundColor='transparent'
/>
</TouchableOpacity>
</View>
);
render() {
return (
<View style={styles.header}>
{this.renderLeft()}
{this.renderTitle()}
{this.renderRight()}
</View>
);
}
}

View File

@ -0,0 +1,49 @@
import { StyleSheet, Platform } from 'react-native';
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
export default StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
flex: 1
},
titleContainer: {
left: TITLE_OFFSET,
right: TITLE_OFFSET,
position: 'absolute',
alignItems: 'center',
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
flexDirection: 'row',
height: 44
},
status: {
borderRadius: 4,
width: 8,
height: 8,
marginRight: 10
},
userStatus: {
fontSize: 10,
color: '#888'
},
title: {
fontWeight: '500',
color: '#292E35'
},
left: {
left: 0,
position: 'absolute'
},
right: {
right: 0,
position: 'absolute',
flexDirection: 'row'
},
headerButton: {
backgroundColor: 'transparent',
height: 44,
width: 44,
alignItems: 'center',
justifyContent: 'center'
}
});

View File

@ -1,54 +1,26 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, View, StyleSheet, Button, SafeAreaView } from 'react-native'; import { Text, View, Button, SafeAreaView } from 'react-native';
import { ListView } from 'realm/react-native'; import { ListView } from 'realm/react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import * as actions from '../actions'; import * as actions from '../../actions';
import { openRoom } from '../actions/room'; import { openRoom } from '../../actions/room';
import { editCancel } from '../actions/messages'; import { editCancel } from '../../actions/messages';
import realm from '../lib/realm'; import realm from '../../lib/realm';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import Message from '../containers/message'; import Message from '../../containers/message';
import MessageActions from '../containers/MessageActions'; import MessageActions from '../../containers/MessageActions';
import MessageBox from '../containers/MessageBox'; import MessageBox from '../../containers/MessageBox';
import Typing from '../containers/Typing'; import Typing from '../../containers/Typing';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from '../../presentation/KeyboardView';
import Header from '../../containers/Header';
import RoomsHeader from './Header';
import styles from './styles';
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id }); const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id });
const styles = StyleSheet.create({
typing: { fontWeight: 'bold', paddingHorizontal: 15, height: 25 },
container: {
flex: 1,
backgroundColor: '#fff'
},
safeAreaView: {
flex: 1
},
list: {
flex: 1,
transform: [{ scaleY: -1 }]
},
separator: {
height: 1,
backgroundColor: '#CED0CE'
},
bannerContainer: {
backgroundColor: 'orange'
},
bannerText: {
margin: 5,
textAlign: 'center',
color: '#a00'
},
loadingMore: {
transform: [{ scaleY: -1 }],
textAlign: 'center',
padding: 5,
color: '#ccc'
}
});
const typing = () => <Typing />; const typing = () => <Typing />;
@connect( @connect(
state => ({ state => ({
@ -78,6 +50,10 @@ export default class RoomView extends React.Component {
loading: PropTypes.bool loading: PropTypes.bool
}; };
static navigationOptions = ({ navigation }) => ({
header: <Header subview={<RoomsHeader navigation={navigation} />} />
});
constructor(props) { constructor(props) {
super(props); super(props);
this.rid = this.rid =

View File

@ -0,0 +1,34 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
typing: { fontWeight: 'bold', paddingHorizontal: 15, height: 25 },
container: {
flex: 1,
backgroundColor: '#fff'
},
safeAreaView: {
flex: 1
},
list: {
flex: 1,
transform: [{ scaleY: -1 }]
},
separator: {
height: 1,
backgroundColor: '#CED0CE'
},
bannerContainer: {
backgroundColor: 'orange'
},
bannerText: {
margin: 5,
textAlign: 'center',
color: '#a00'
},
loadingMore: {
transform: [{ scaleY: -1 }],
textAlign: 'center',
padding: 5,
color: '#ccc'
}
});

View File

@ -0,0 +1,210 @@
import React from 'react';
import { Text, View, Platform, TouchableOpacity, TextInput } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Modal from 'react-native-modal';
import { CachedImage } from 'react-native-img-cache';
import { HeaderBackButton } from 'react-navigation';
import Avatar from '../../../containers/Avatar';
import RocketChat from '../../../lib/rocketchat';
import { STATUS_COLORS } from '../../../constants/colors';
import { setSearch } from '../../../actions/rooms';
import styles from './styles';
@connect(state => ({
user: state.login.user,
baseUrl: state.settings.Site_Url
}), dispatch => ({
setSearch: searchText => dispatch(setSearch(searchText))
}))
export default class extends React.Component {
static propTypes = {
navigation: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
baseUrl: PropTypes.string,
setSearch: PropTypes.func
}
constructor(props) {
super(props);
this.state = {
isModalVisible: false,
searching: false
};
}
onPressModalButton(status) {
RocketChat.setUserPresenceDefaultStatus(status);
this.hideModal();
}
onSearchChangeText(text) {
this.props.setSearch(text.trim());
}
onPressCancelSearchButton() {
this.setState({ searching: false });
this.props.setSearch('');
}
onPressSearchButton() {
this.setState({ searching: true });
requestAnimationFrame(() => {
this.inputSearch.focus();
});
}
showModal() {
this.setState({ isModalVisible: true });
}
hideModal() {
this.setState({ isModalVisible: false });
}
createChannel() {
this.props.navigation.navigate('SelectUsers');
}
renderLeft() {
return (
<View style={styles.left}>
<TouchableOpacity
style={styles.headerButton}
onPress={() => this.props.navigation.navigate('DrawerOpen')}
>
<CachedImage
style={styles.serverImage}
source={{ uri: encodeURI(`${ this.props.baseUrl }/assets/favicon_32.png`) }}
/>
</TouchableOpacity>
</View>
);
}
renderTitle() {
if (!this.props.user.username) {
return null;
}
return (
<TouchableOpacity style={styles.titleContainer} onPress={() => this.showModal()}>
<View style={[styles.status, { backgroundColor: STATUS_COLORS[this.props.user.status || 'offline'] }]} />
<Avatar
text={this.props.user.username}
size={24}
style={{ marginRight: 5 }}
baseUrl={this.props.baseUrl}
/>
<Text style={styles.title}>{this.props.user.username}</Text>
</TouchableOpacity>
);
}
renderRight() {
return (
<View style={styles.right}>
{Platform.OS === 'android' ?
<TouchableOpacity
style={styles.headerButton}
onPress={() => this.onPressSearchButton()}
>
<Icon
name='md-search'
color='#292E35'
size={24}
backgroundColor='transparent'
/>
</TouchableOpacity> : null}
{Platform.OS === 'ios' ?
<TouchableOpacity
style={styles.headerButton}
onPress={() => this.createChannel()}
>
<Icon
name='ios-add'
color='#292E35'
size={24}
backgroundColor='transparent'
/>
</TouchableOpacity> : null}
</View>
);
}
renderModalButton = (status, text) => {
const statusStyle = [styles.status, { backgroundColor: STATUS_COLORS[status] }];
const textStyle = { flex: 1, fontWeight: this.props.user.status === status ? 'bold' : 'normal' };
return (
<TouchableOpacity
style={styles.modalButton}
onPress={() => this.onPressModalButton(status)}
>
<View style={statusStyle} />
<Text style={textStyle}>
{text || status.charAt(0).toUpperCase() + status.slice(1)}
</Text>
</TouchableOpacity>
);
};
renderHeader() {
if (this.state.searching) {
return null;
}
return (
<View style={styles.header}>
{this.renderLeft()}
{this.renderTitle()}
{this.renderRight()}
</View>
);
}
renderSearch() {
if (!this.state.searching) {
return null;
}
return (
<View style={styles.header}>
<View style={styles.left}>
<HeaderBackButton onPress={() => this.onPressCancelSearchButton()} />
</View>
<TextInput
ref={inputSearch => this.inputSearch = inputSearch}
underlineColorAndroid='transparent'
style={styles.inputSearch}
onChangeText={text => this.onSearchChangeText(text)}
returnKeyType='search'
placeholder='Search'
clearButtonMode='while-editing'
blurOnSubmit
/>
</View>
);
}
render() {
return (
<View style={styles.header}>
{this.renderHeader()}
{this.renderSearch()}
<Modal
isVisible={this.state.isModalVisible}
supportedOrientations={['portrait', 'landscape']}
style={{ alignItems: 'center' }}
onModalHide={() => this.hideModal()}
onBackdropPress={() => this.hideModal()}
>
<View style={styles.modal}>
{this.renderModalButton('online')}
{this.renderModalButton('busy')}
{this.renderModalButton('away')}
{this.renderModalButton('offline', 'Invisible')}
</View>
</Modal>
</View>
);
}
}

View File

@ -0,0 +1,75 @@
import { StyleSheet, Platform, Dimensions } from 'react-native';
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
const { width } = Dimensions.get('window');
export default StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
flex: 1
},
titleContainer: {
left: TITLE_OFFSET,
right: TITLE_OFFSET,
position: 'absolute',
alignItems: 'center',
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
flexDirection: 'row',
height: 44
},
status: {
borderRadius: 4,
width: 8,
height: 8,
marginRight: 10
},
avatar: {
marginRight: 10
},
title: {
fontWeight: '500',
color: '#292E35'
},
left: {
left: 0,
position: 'absolute'
},
right: {
right: 0,
position: 'absolute',
flexDirection: 'row'
},
modal: {
width: width - 60,
height: width - 60,
backgroundColor: '#F7F7F7',
borderRadius: 4,
flexDirection: 'column'
},
modalButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0, 0, 0, .3)',
paddingHorizontal: 20
},
headerButton: {
backgroundColor: 'transparent',
height: 44,
width: 44,
alignItems: 'center',
justifyContent: 'center'
},
serverImage: {
width: 24,
height: 24,
borderRadius: 4
},
inputSearch: {
flex: 1,
marginLeft: 44
}
});

View File

@ -3,69 +3,26 @@ import { ListView } from 'realm/react-native';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import { Platform, View, StyleSheet, TextInput, SafeAreaView } from 'react-native'; import { Platform, View, TextInput, SafeAreaView } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as actions from '../actions'; import * as actions from '../../actions';
import * as server from '../actions/connect'; import * as server from '../../actions/connect';
import realm from '../lib/realm'; import realm from '../../lib/realm';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import RoomItem from '../presentation/RoomItem'; import RoomItem from '../../presentation/RoomItem';
import Banner from '../containers/Banner'; import Banner from '../../containers/Banner';
import { goRoom } from '../containers/routes/NavigationService'; import { goRoom } from '../../containers/routes/NavigationService';
import Header from '../../containers/Header';
const styles = StyleSheet.create({ import RoomsListHeader from './Header';
container: { import styles from './styles';
flex: 1,
alignItems: 'stretch',
justifyContent: 'center'
},
separator: {
height: 1,
backgroundColor: '#E7E7E7'
},
list: {
width: '100%',
backgroundColor: '#FFFFFF'
},
emptyView: {
flexGrow: 1,
alignItems: 'stretch',
justifyContent: 'center'
},
emptyText: {
textAlign: 'center',
fontSize: 18,
color: '#ccc'
},
actionButtonIcon: {
fontSize: 20,
height: 22,
color: 'white'
},
searchBoxView: {
backgroundColor: '#eee'
},
searchBox: {
backgroundColor: '#fff',
margin: 5,
borderRadius: 5,
padding: 5,
paddingLeft: 10,
color: '#aaa'
},
safeAreaView: {
flex: 1,
backgroundColor: '#fff'
}
});
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }); const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
@connect(state => ({ @connect(state => ({
server: state.server.server, server: state.server.server,
login: state.login, login: state.login,
Site_Url: state.settings.Site_Url, Site_Url: state.settings.Site_Url,
canShowList: state.login.token || state.login.user.token canShowList: state.login.token || state.login.user.token,
// Message_DateFormat: state.settings.Message_DateFormat searchText: state.rooms.searchText
}), dispatch => ({ }), dispatch => ({
login: () => dispatch(actions.login()), login: () => dispatch(actions.login()),
connect: () => dispatch(server.connectRequest()) connect: () => dispatch(server.connectRequest())
@ -75,10 +32,14 @@ export default class RoomsListView extends React.Component {
static propTypes = { static propTypes = {
navigation: PropTypes.object.isRequired, navigation: PropTypes.object.isRequired,
Site_Url: PropTypes.string, Site_Url: PropTypes.string,
// Message_DateFormat: PropTypes.string, server: PropTypes.string,
server: PropTypes.string searchText: PropTypes.string
} }
static navigationOptions = ({ navigation }) => ({
header: <Header subview={<RoomsListHeader navigation={navigation} />} />
});
constructor(props) { constructor(props) {
super(props); super(props);
@ -104,6 +65,8 @@ export default class RoomsListView extends React.Component {
this.data.removeListener(this.updateState); this.data.removeListener(this.updateState);
this.data = realm.objects('subscriptions').filtered('_server.id = $0', props.server).sorted('roomUpdatedAt', true); this.data = realm.objects('subscriptions').filtered('_server.id = $0', props.server).sorted('roomUpdatedAt', true);
this.data.addListener(this.updateState); this.data.addListener(this.updateState);
} else if (this.props.searchText !== props.searchText) {
this.search(props.searchText);
} }
} }
@ -111,11 +74,13 @@ export default class RoomsListView extends React.Component {
this.data.removeAllListeners(); this.data.removeAllListeners();
} }
onSearchChangeText = (text) => { onSearchChangeText(text) {
this.setState({ searchText: text });
this.search(text);
}
search(text) {
const searchText = text.trim(); const searchText = text.trim();
this.setState({
searchText: text
});
if (searchText === '') { if (searchText === '') {
return this.setState({ return this.setState({
dataSource: ds.cloneWithRows(this.data) dataSource: ds.cloneWithRows(this.data)
@ -222,7 +187,7 @@ export default class RoomsListView extends React.Component {
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
style={styles.searchBox} style={styles.searchBox}
value={this.state.searchText} value={this.state.searchText}
onChangeText={this.onSearchChangeText} onChangeText={text => this.onSearchChangeText(text)}
returnKeyType='search' returnKeyType='search'
placeholder='Search' placeholder='Search'
clearButtonMode='while-editing' clearButtonMode='while-editing'
@ -251,8 +216,8 @@ export default class RoomsListView extends React.Component {
dataSource={this.state.dataSource} dataSource={this.state.dataSource}
style={styles.list} style={styles.list}
renderRow={this.renderItem} renderRow={this.renderItem}
renderHeader={this.renderSearchBar} renderHeader={Platform.OS === 'ios' ? this.renderSearchBar : null}
contentOffset={{ x: 0, y: 38 }} contentOffset={Platform.OS === 'ios' ? { x: 0, y: 38 } : {}}
enableEmptySections enableEmptySections
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
/> />

View File

@ -0,0 +1,47 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
container: {
flex: 1,
alignItems: 'stretch',
justifyContent: 'center'
},
separator: {
height: 1,
backgroundColor: '#E7E7E7'
},
list: {
width: '100%',
backgroundColor: '#FFFFFF'
},
emptyView: {
flexGrow: 1,
alignItems: 'stretch',
justifyContent: 'center'
},
emptyText: {
textAlign: 'center',
fontSize: 18,
color: '#ccc'
},
actionButtonIcon: {
fontSize: 20,
height: 22,
color: 'white'
},
searchBoxView: {
backgroundColor: '#eee'
},
searchBox: {
backgroundColor: '#fff',
margin: 5,
borderRadius: 5,
padding: 5,
paddingLeft: 10,
color: '#aaa'
},
safeAreaView: {
flex: 1,
backgroundColor: '#fff'
}
});