Room actions (#231)

* Layout


* Empty starred list


* Favorite room

* Pinned messages

* fix last messages

* fix date on pinned messages
This commit is contained in:
Diego Mello 2018-02-19 18:19:39 -03:00 committed by Guilherme Gazzo
parent bb5e29fdc7
commit b1bb815b07
24 changed files with 739 additions and 25 deletions

View File

@ -41,8 +41,8 @@ jobs:
- image: circleci/android:api-26-alpha - image: circleci/android:api-26-alpha
environment: environment:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError" GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"
JVM_OPTS: -Xmx2048m JVM_OPTS: -Xmx4096m
TERM: dumb TERM: dumb
BASH_ENV: "~/.nvm/nvm.sh" BASH_ENV: "~/.nvm/nvm.sh"

View File

@ -89,6 +89,8 @@ export const SERVER = createRequestTypes('SERVER', [
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT', 'DISCONNECT_BY_USER']); export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT', 'DISCONNECT_BY_USER']);
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 ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET', 'REQUEST']);
export const STARRED_MESSAGES = createRequestTypes('STARRED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGE_RECEIVED', 'MESSAGE_UNSTARRED']);
export const PINNED_MESSAGES = createRequestTypes('PINNED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGE_RECEIVED', 'MESSAGE_UNPINNED']);
export const INCREMENT = 'INCREMENT'; export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT'; export const DECREMENT = 'DECREMENT';

View File

@ -0,0 +1,28 @@
import * as types from './actionsTypes';
export function openPinnedMessages(rid) {
return {
type: types.PINNED_MESSAGES.OPEN,
rid
};
}
export function closePinnedMessages() {
return {
type: types.PINNED_MESSAGES.CLOSE
};
}
export function pinnedMessageReceived(message) {
return {
type: types.PINNED_MESSAGES.MESSAGE_RECEIVED,
message
};
}
export function pinnedMessageUnpinned(messageId) {
return {
type: types.PINNED_MESSAGES.MESSAGE_UNPINNED,
messageId
};
}

View File

@ -0,0 +1,28 @@
import * as types from './actionsTypes';
export function openStarredMessages(rid) {
return {
type: types.STARRED_MESSAGES.OPEN,
rid
};
}
export function closeStarredMessages() {
return {
type: types.STARRED_MESSAGES.CLOSE
};
}
export function starredMessageReceived(message) {
return {
type: types.STARRED_MESSAGES.MESSAGE_RECEIVED,
message
};
}
export function starredMessageUnstarred(messageId) {
return {
type: types.STARRED_MESSAGES.MESSAGE_UNSTARRED,
messageId
};
}

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, TouchableHighlight, Text, TouchableOpacity, Vibration } from 'react-native'; import { View, TouchableHighlight, Text, TouchableOpacity, Vibration, ViewPropTypes } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment'; import moment from 'moment';
@ -33,17 +33,18 @@ import styles from './styles';
export default class Message extends React.Component { export default class Message extends React.Component {
static propTypes = { static propTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
reactions: PropTypes.object.isRequired, reactions: PropTypes.any.isRequired,
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
Message_TimeFormat: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired,
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired,
user: PropTypes.object.isRequired, user: PropTypes.object.isRequired,
editing: PropTypes.bool, editing: PropTypes.bool,
actionsShow: PropTypes.func,
errorActionsShow: PropTypes.func, errorActionsShow: PropTypes.func,
customEmojis: PropTypes.object, customEmojis: PropTypes.object,
toggleReactionPicker: PropTypes.func, toggleReactionPicker: PropTypes.func,
onReactionPress: PropTypes.func onReactionPress: PropTypes.func,
style: ViewPropTypes.style,
onLongPress: PropTypes.func
} }
constructor(props) { constructor(props) {
@ -73,7 +74,7 @@ export default class Message extends React.Component {
} }
onLongPress() { onLongPress() {
this.props.actionsShow(this.parseMessage()); this.props.onLongPress(this.parseMessage());
} }
onErrorPress() { onErrorPress() {
@ -222,7 +223,7 @@ export default class Message extends React.Component {
render() { render() {
const { const {
item, message, editing, baseUrl, customEmojis item, message, editing, baseUrl, customEmojis, style
} = this.props; } = this.props;
const username = item.alias || item.u.username; const username = item.alias || item.u.username;
const isEditing = message._id === item._id && editing; const isEditing = message._id === item._id && editing;
@ -235,7 +236,7 @@ export default class Message extends React.Component {
disabled={this.isDeleted() || this.hasError()} disabled={this.isDeleted() || this.hasError()}
underlayColor='#FFFFFF' underlayColor='#FFFFFF'
activeOpacity={0.3} activeOpacity={0.3}
style={[styles.message, isEditing ? styles.editing : null]} style={[styles.message, isEditing ? styles.editing : null, style]}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
> >
<View style={styles.flex}> <View style={styles.flex}>

View File

@ -4,9 +4,12 @@ import { StackNavigator, DrawerNavigator } from 'react-navigation';
import Sidebar from '../../containers/Sidebar'; import Sidebar from '../../containers/Sidebar';
import RoomsListView from '../../views/RoomsListView'; import RoomsListView from '../../views/RoomsListView';
import RoomView from '../../views/RoomView'; import RoomView from '../../views/RoomView';
import RoomActionsView from '../../views/RoomActionsView';
import CreateChannelView from '../../views/CreateChannelView'; import CreateChannelView from '../../views/CreateChannelView';
import SelectUsersView from '../../views/SelectUsersView'; import SelectUsersView from '../../views/SelectUsersView';
import NewServerView from '../../views/NewServerView'; import NewServerView from '../../views/NewServerView';
import StarredMessagesView from '../../views/StarredMessagesView';
import PinnedMessagesView from '../../views/PinnedMessagesView';
const AuthRoutes = StackNavigator( const AuthRoutes = StackNavigator(
{ {
@ -33,6 +36,27 @@ const AuthRoutes = StackNavigator(
navigationOptions: { navigationOptions: {
title: 'New server' title: 'New server'
} }
},
RoomActions: {
screen: RoomActionsView,
navigationOptions: {
title: 'Actions',
headerTintColor: '#292E35'
}
},
StarredMessages: {
screen: StarredMessagesView,
navigationOptions: {
title: 'Starred Messages',
headerTintColor: '#292E35'
}
},
PinnedMessages: {
screen: PinnedMessagesView,
navigationOptions: {
title: 'Pinned Messages',
headerTintColor: '#292E35'
}
} }
}, },
{ {

View File

@ -1,4 +1,4 @@
import { createStore, applyMiddleware } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger'; import logger from 'redux-logger';
import { composeWithDevTools } from 'remote-redux-devtools'; import { composeWithDevTools } from 'remote-redux-devtools';
@ -13,14 +13,15 @@ if (__DEV__) {
/* eslint-disable global-require */ /* eslint-disable global-require */
const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default(); const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default();
enhacers = composeWithDevTools( const devComposer = composeWithDevTools({ hostname: 'localhost', port: 8000 });
enhacers = devComposer(
applyAppStateListener(), applyAppStateListener(),
applyMiddleware(reduxImmutableStateInvariant), applyMiddleware(reduxImmutableStateInvariant),
applyMiddleware(sagaMiddleware), applyMiddleware(sagaMiddleware),
applyMiddleware(logger) applyMiddleware(logger)
); );
} else { } else {
enhacers = composeWithDevTools( enhacers = compose(
applyAppStateListener(), applyAppStateListener(),
applyMiddleware(sagaMiddleware) applyMiddleware(sagaMiddleware)
); );

View File

@ -82,6 +82,7 @@ const subscriptionSchema = {
// groupMentions: 0, // groupMentions: 0,
roomUpdatedAt: { type: 'date', optional: true }, roomUpdatedAt: { type: 'date', optional: true },
ro: { type: 'bool', optional: true }, ro: { type: 'bool', optional: true },
lastOpen: { type: 'date', optional: true },
lastMessage: { type: 'messages', optional: true } lastMessage: { type: 'messages', optional: true }
} }
}; };

View File

@ -13,6 +13,8 @@ import { someoneTyping, roomMessageReceived } from '../actions/room';
import { setUser } from '../actions/login'; import { setUser } from '../actions/login';
import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect'; import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect';
import { requestActiveUser } from '../actions/activeUsers'; import { requestActiveUser } from '../actions/activeUsers';
import { starredMessageReceived, starredMessageUnstarred } from '../actions/starredMessages';
import { pinnedMessageReceived, pinnedMessageUnpinned } from '../actions/pinnedMessages';
import Ddp from './ddp'; import Ddp from './ddp';
export { Accounts } from 'react-native-meteor'; export { Accounts } from 'react-native-meteor';
@ -145,6 +147,30 @@ const RocketChat = {
}); });
} }
}); });
this.ddp.on('rocketchat_starred_message', (ddpMessage) => {
if (ddpMessage.msg === 'added') {
const message = ddpMessage.fields;
message._id = ddpMessage.id;
const starredMessage = this._buildMessage(message);
return reduxStore.dispatch(starredMessageReceived(starredMessage));
}
if (ddpMessage.msg === 'removed') {
return reduxStore.dispatch(starredMessageUnstarred(ddpMessage.id));
}
});
this.ddp.on('rocketchat_pinned_message', (ddpMessage) => {
if (ddpMessage.msg === 'added') {
const message = ddpMessage.fields;
message._id = ddpMessage.id;
const pinnedMessage = this._buildMessage(message);
return reduxStore.dispatch(pinnedMessageReceived(pinnedMessage));
}
if (ddpMessage.msg === 'removed') {
return reduxStore.dispatch(pinnedMessageUnpinned(ddpMessage.id));
}
});
}).catch(console.log); }).catch(console.log);
}, },
@ -272,9 +298,7 @@ const RocketChat = {
_buildMessage(message) { _buildMessage(message) {
message.status = messagesStatus.SENT; message.status = messagesStatus.SENT;
normalizeMessage(message); normalizeMessage(message);
if (message.urls) { message.urls = message.urls ? RocketChat._parseUrls(message.urls) : [];
message.urls = RocketChat._parseUrls(message.urls);
}
// loadHistory returns message.starred as object // loadHistory returns message.starred as object
// stream-room-messages returns message.starred as an array // stream-room-messages returns message.starred as an array
message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred); message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred);
@ -358,8 +382,15 @@ const RocketChat = {
createDirectMessage(username) { createDirectMessage(username) {
return call('createDirectMessage', username); return call('createDirectMessage', username);
}, },
readMessages(rid) { async readMessages(rid) {
return call('readMessages', rid); const ret = await call('readMessages', rid);
const [subscription] = database.objects('subscriptions').filtered('rid = $0', rid);
database.write(() => {
subscription.lastOpen = new Date();
});
return ret;
}, },
joinRoom(rid) { joinRoom(rid) {
return call('joinRoom', rid); return call('joinRoom', rid);
@ -610,6 +641,9 @@ const RocketChat = {
}, },
setReaction(emoji, messageId) { setReaction(emoji, messageId) {
return call('setReaction', emoji, messageId); return call('setReaction', emoji, messageId);
},
toggleFavorite(rid, f) {
return call('toggleFavorite', rid, !f);
} }
}; };

View File

@ -12,6 +12,8 @@ import app from './app';
import permissions from './permissions'; import permissions from './permissions';
import customEmojis from './customEmojis'; import customEmojis from './customEmojis';
import activeUsers from './activeUsers'; import activeUsers from './activeUsers';
import starredMessages from './starredMessages';
import pinnedMessages from './pinnedMessages';
export default combineReducers({ export default combineReducers({
settings, settings,
@ -26,5 +28,7 @@ export default combineReducers({
rooms, rooms,
permissions, permissions,
customEmojis, customEmojis,
activeUsers activeUsers,
starredMessages,
pinnedMessages
}); });

View File

@ -0,0 +1,24 @@
import { PINNED_MESSAGES } from '../actions/actionsTypes';
const initialState = {
messages: []
};
export default function server(state = initialState, action) {
switch (action.type) {
case PINNED_MESSAGES.MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
};
case PINNED_MESSAGES.MESSAGE_UNPINNED:
return {
...state,
messages: state.messages.filter(message => message._id !== action.messageId)
};
case PINNED_MESSAGES.CLOSE:
return initialState;
default:
return state;
}
}

View File

@ -0,0 +1,24 @@
import { STARRED_MESSAGES } from '../actions/actionsTypes';
const initialState = {
messages: []
};
export default function server(state = initialState, action) {
switch (action.type) {
case STARRED_MESSAGES.MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
};
case STARRED_MESSAGES.MESSAGE_UNSTARRED:
return {
...state,
messages: state.messages.filter(message => message._id !== action.messageId)
};
case STARRED_MESSAGES.CLOSE:
return initialState;
default:
return state;
}
}

View File

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

View File

@ -0,0 +1,14 @@
import { take, takeLatest } from 'redux-saga/effects';
import * as types from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat';
const watchPinnedMessagesRoom = function* watchPinnedMessagesRoom({ rid }) {
const sub = yield RocketChat.subscribe('pinnedMessages', rid, 50);
yield take(types.PINNED_MESSAGES.CLOSE);
sub.unsubscribe().catch(e => alert(e));
};
const root = function* root() {
yield takeLatest(types.PINNED_MESSAGES.OPEN, watchPinnedMessagesRoom);
};
export default root;

View File

@ -0,0 +1,14 @@
import { take, takeLatest } from 'redux-saga/effects';
import * as types from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat';
const watchStarredMessagesRoom = function* watchStarredMessagesRoom({ rid }) {
const sub = yield RocketChat.subscribe('starredMessages', rid, 50);
yield take(types.STARRED_MESSAGES.CLOSE);
sub.unsubscribe().catch(e => alert(e));
};
const root = function* root() {
yield takeLatest(types.STARRED_MESSAGES.OPEN, watchStarredMessagesRoom);
};
export default root;

View File

@ -0,0 +1,111 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FlatList, Text, View } from 'react-native';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet';
import { openPinnedMessages, closePinnedMessages } from '../../actions/pinnedMessages';
import styles from './styles';
import Message from '../../containers/message';
import { togglePinRequest } from '../../actions/messages';
const PIN_INDEX = 0;
const CANCEL_INDEX = 1;
const options = ['Unpin', 'Cancel'];
@connect(
state => ({
messages: state.pinnedMessages.messages,
user: state.login.user,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}),
dispatch => ({
openPinnedMessages: rid => dispatch(openPinnedMessages(rid)),
closePinnedMessages: () => dispatch(closePinnedMessages()),
togglePinRequest: message => dispatch(togglePinRequest(message))
})
)
export default class PinnedMessagesView extends React.PureComponent {
static propTypes = {
navigation: PropTypes.object,
messages: PropTypes.array,
user: PropTypes.object,
baseUrl: PropTypes.string,
openPinnedMessages: PropTypes.func,
closePinnedMessages: PropTypes.func,
togglePinRequest: PropTypes.func
}
constructor(props) {
super(props);
this.state = {
message: {}
};
}
componentWillMount() {
this.props.openPinnedMessages(this.props.navigation.state.params.rid);
}
componentWillUnmount() {
this.props.closePinnedMessages();
}
onLongPress = (message) => {
this.setState({ message });
this.actionSheet.show();
}
handleActionPress = (actionIndex) => {
switch (actionIndex) {
case PIN_INDEX:
this.props.togglePinRequest(this.state.message);
break;
default:
break;
}
}
renderEmpty = () => (
<View style={styles.listEmptyContainer}>
<Text>No pinned messages</Text>
</View>
)
renderItem = ({ item }) => (
<Message
item={item}
style={styles.message}
reactions={item.reactions}
user={this.props.user}
baseUrl={this.props.baseUrl}
Message_TimeFormat='MMMM Do YYYY, h:mm:ss a'
onLongPress={this.onLongPress}
/>
)
render() {
if (this.props.messages.length === 0) {
return this.renderEmpty();
}
return (
[
<FlatList
key='pinned-messages-view-list'
data={this.props.messages}
renderItem={this.renderItem}
style={styles.list}
keyExtractor={item => item._id}
/>,
<ActionSheet
key='pinned-messages-view-action-sheet'
ref={o => this.actionSheet = o}
title='Actions'
options={options}
cancelButtonIndex={CANCEL_INDEX}
onPress={this.handleActionPress}
/>
]
);
}
}

View File

@ -0,0 +1,17 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
list: {
flex: 1,
backgroundColor: '#ffffff'
},
message: {
transform: [{ scaleY: 1 }]
},
listEmptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff'
}
});

View File

@ -0,0 +1,175 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, SectionList, Text, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import { connect } from 'react-redux';
import styles from './styles';
import Avatar from '../../containers/Avatar';
import Touch from '../../utils/touch';
import database from '../../lib/realm';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class RoomActionsView extends React.PureComponent {
static propTypes = {
baseUrl: PropTypes.string,
navigation: PropTypes.object
}
constructor(props) {
super(props);
const { rid } = props.navigation.state.params;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.state = {
sections: [],
room: {}
};
}
componentWillMount() {
this.updateRoom();
this.updateSections();
}
componentDidMount() {
this.rooms.addListener(this.updateRoom);
}
updateRoom = () => {
const [room] = this.rooms;
this.setState({ room });
this.props.navigation.setParams({
f: room.f
});
this.updateSections();
}
updateSections = () => {
const { rid, t } = this.state.room;
const sections = [{
data: [{ icon: 'ios-star', name: 'USER' }],
renderItem: this.renderRoomInfo
}, {
data: [
{ icon: 'ios-call-outline', name: 'Voice call' },
{ icon: 'ios-videocam-outline', name: 'Video call' }
],
renderItem: this.renderItem
}, {
data: [
{ icon: 'ios-attach', name: 'Files' },
{ icon: 'ios-at-outline', name: 'Mentions' },
{
icon: 'ios-star-outline',
name: 'Starred',
route: 'StarredMessages',
params: { rid }
},
{ icon: 'ios-search', name: 'Search' },
{ icon: 'ios-share-outline', name: 'Share' },
{
icon: 'ios-pin',
name: 'Pinned',
route: 'PinnedMessages',
params: { rid }
},
{ icon: 'ios-code', name: 'Snippets' },
{ icon: 'ios-notifications-outline', name: 'Notifications preferences' }
],
renderItem: this.renderItem
}];
if (t === 'd') {
sections.push({
data: [
{ icon: 'ios-volume-off', name: 'Mute user' },
{ icon: 'block', name: 'Block user', type: 'danger' }
],
renderItem: this.renderItem
});
} else if (t === 'c' || t === 'p') {
sections[2].data.unshift({ icon: 'ios-people', name: 'Members', description: '42 members' });
sections.push({
data: [
{ icon: 'ios-volume-off', name: 'Mute channel' },
{ icon: 'block', name: 'Leave channel', type: 'danger' }
],
renderItem: this.renderItem
});
}
this.setState({ sections });
}
renderRoomInfo = ({ item }) => this.renderTouchableItem([
<Avatar
key='avatar'
text={this.state.room.name}
size={50}
style={StyleSheet.flatten(styles.avatar)}
baseUrl={this.props.baseUrl}
type={this.state.room.t}
/>,
<View key='name' style={styles.roomTitleContainer}>
<Text style={styles.roomTitle}>{this.state.room.fname}</Text>
<Text style={styles.roomDescription}>@{this.state.room.name}</Text>
</View>,
<Icon key='icon' name='ios-arrow-forward' size={20} style={styles.sectionItemIcon} color='#cbced1' />
], item)
renderTouchableItem = (subview, item) => (
<Touch
onPress={() => item.route && this.props.navigation.navigate(item.route, item.params)}
underlayColor='#FFFFFF'
activeOpacity={0.5}
accessibilityLabel={item.name}
accessibilityTraits='button'
>
<View style={styles.sectionItem}>
{subview}
</View>
</Touch>
)
renderItem = ({ item }) => {
if (item.type === 'danger') {
const subview = [
<MaterialIcon key='icon' name={item.icon} size={20} style={[styles.sectionItemIcon, styles.textColorDanger]} />,
<Text key='name' style={[styles.sectionItemName, styles.textColorDanger]}>{ item.name }</Text>
];
return this.renderTouchableItem(subview, item);
}
const subview = [
<Icon key='left-icon' name={item.icon} size={24} style={styles.sectionItemIcon} />,
<Text key='name' style={styles.sectionItemName}>{ item.name }</Text>,
item.description && <Text key='description' style={styles.sectionItemDescription}>{ item.description }</Text>,
<Icon key='right-icon' name='ios-arrow-forward' size={20} style={styles.sectionItemIcon} color='#cbced1' />
];
return this.renderTouchableItem(subview, item);
}
renderSectionSeparator = (data) => {
if (!data.trailingItem) {
if (!data.trailingSection) {
return <View style={styles.sectionSeparatorBorder} />;
}
return null;
}
return (
<View style={[styles.sectionSeparator, data.leadingSection && styles.sectionSeparatorBorder]} />
);
}
render() {
return (
<SectionList
style={styles.container}
stickySectionHeadersEnabled={false}
sections={this.state.sections}
SectionSeparatorComponent={this.renderSectionSeparator}
keyExtractor={(item, index) => index}
/>
);
}
}

View File

@ -0,0 +1,54 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
container: {
backgroundColor: '#F6F7F9'
},
headerButton: {
backgroundColor: 'transparent',
height: 44,
width: 44,
alignItems: 'center',
justifyContent: 'center'
},
sectionItem: {
backgroundColor: '#ffffff',
paddingVertical: 10,
flexDirection: 'row',
alignItems: 'center'
},
sectionItemIcon: {
width: 45,
textAlign: 'center'
},
sectionItemName: {
flex: 1
},
sectionItemDescription: {
color: '#cbced1'
},
sectionSeparator: {
height: 10,
backgroundColor: '#F6F7F9'
},
sectionSeparatorBorder: {
borderColor: '#EBEDF1',
borderTopWidth: 1
},
textColorDanger: {
color: '#f5455c'
},
avatar: {
marginHorizontal: 10
},
roomTitleContainer: {
flex: 1
},
roomTitle: {
fontSize: 16
},
roomDescription: {
fontSize: 12,
color: '#cbced1'
}
});

View File

@ -5,11 +5,14 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { HeaderBackButton } from 'react-navigation'; import { HeaderBackButton } from 'react-navigation';
import RocketChat from '../../../lib/rocketchat';
import realm from '../../../lib/realm'; import realm from '../../../lib/realm';
import Avatar from '../../../containers/Avatar'; import Avatar from '../../../containers/Avatar';
import { STATUS_COLORS } from '../../../constants/colors'; import { STATUS_COLORS } from '../../../constants/colors';
import styles from './styles'; import styles from './styles';
import { closeRoom } from '../../../actions/room'; import { closeRoom } from '../../../actions/room';
import Touch from '../../../utils/touch';
@connect(state => ({ @connect(state => ({
user: state.login.user, user: state.login.user,
@ -108,9 +111,25 @@ export default class RoomHeaderView extends React.PureComponent {
renderRight = () => ( renderRight = () => (
<View style={styles.right}> <View style={styles.right}>
<Touch
onPress={() => RocketChat.toggleFavorite(this.room[0].rid, this.room[0].f)}
accessibilityLabel='Star room'
accessibilityTraits='button'
underlayColor='#FFFFFF'
activeOpacity={0.5}
>
<View style={styles.headerButton}>
<Icon
name={`${ Platform.OS === 'ios' ? 'ios' : 'md' }-star${ this.room[0].f ? '' : '-outline' }`}
color='#f6c502'
size={24}
backgroundColor='transparent'
/>
</View>
</Touch>
<TouchableOpacity <TouchableOpacity
style={styles.headerButton} style={styles.headerButton}
onPress={() => {}} onPress={() => this.props.navigation.navigate('RoomActions', { rid: this.room[0].rid })}
accessibilityLabel='Room actions' accessibilityLabel='Room actions'
accessibilityTraits='button' accessibilityTraits='button'
> >

View File

@ -42,7 +42,7 @@ export default StyleSheet.create({
headerButton: { headerButton: {
backgroundColor: 'transparent', backgroundColor: 'transparent',
height: 44, height: 44,
width: 44, width: 40,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center' justifyContent: 'center'
} }

View File

@ -8,7 +8,7 @@ import equal from 'deep-equal';
import { List } from './ListView'; import { List } from './ListView';
import * as actions from '../../actions'; import * as actions from '../../actions';
import { openRoom, setLastOpen } from '../../actions/room'; import { openRoom, setLastOpen } from '../../actions/room';
import { editCancel, toggleReactionPicker } from '../../actions/messages'; import { editCancel, toggleReactionPicker, actionsShow } from '../../actions/messages';
import database from '../../lib/realm'; import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import Message from '../../containers/message'; import Message from '../../containers/message';
@ -35,7 +35,8 @@ import styles from './styles';
openRoom: room => dispatch(openRoom(room)), openRoom: room => dispatch(openRoom(room)),
editCancel: () => dispatch(editCancel()), editCancel: () => dispatch(editCancel()),
setLastOpen: date => dispatch(setLastOpen(date)), setLastOpen: date => dispatch(setLastOpen(date)),
toggleReactionPicker: message => dispatch(toggleReactionPicker(message)) toggleReactionPicker: message => dispatch(toggleReactionPicker(message)),
actionsShow: actionMessage => dispatch(actionsShow(actionMessage))
}) })
) )
export default class RoomView extends React.Component { export default class RoomView extends React.Component {
@ -51,8 +52,9 @@ export default class RoomView extends React.Component {
Message_TimeFormat: PropTypes.string, Message_TimeFormat: PropTypes.string,
loading: PropTypes.bool, loading: PropTypes.bool,
actionMessage: PropTypes.object, actionMessage: PropTypes.object,
toggleReactionPicker: PropTypes.func.isRequired toggleReactionPicker: PropTypes.func.isRequired,
// layoutAnimation: PropTypes.instanceOf(Date) // layoutAnimation: PropTypes.instanceOf(Date),
actionsShow: PropTypes.func
}; };
static navigationOptions = ({ navigation }) => ({ static navigationOptions = ({ navigation }) => ({
@ -124,6 +126,10 @@ export default class RoomView extends React.Component {
}); });
} }
onMessageLongPress = (message) => {
this.props.actionsShow(message);
}
onReactionPress = (shortname, messageId) => { onReactionPress = (shortname, messageId) => {
if (!messageId) { if (!messageId) {
RocketChat.setReaction(shortname, this.props.actionMessage._id); RocketChat.setReaction(shortname, this.props.actionMessage._id);
@ -158,6 +164,7 @@ export default class RoomView extends React.Component {
Message_TimeFormat={this.props.Message_TimeFormat} Message_TimeFormat={this.props.Message_TimeFormat}
user={this.props.user} user={this.props.user}
onReactionPress={this.onReactionPress} onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
/> />
); );

View File

@ -0,0 +1,111 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FlatList, Text, View } from 'react-native';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet';
import { openStarredMessages, closeStarredMessages } from '../../actions/starredMessages';
import styles from './styles';
import Message from '../../containers/message';
import { toggleStarRequest } from '../../actions/messages';
const STAR_INDEX = 0;
const CANCEL_INDEX = 1;
const options = ['Unstar', 'Cancel'];
@connect(
state => ({
messages: state.starredMessages.messages,
user: state.login.user,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}),
dispatch => ({
openStarredMessages: rid => dispatch(openStarredMessages(rid)),
closeStarredMessages: () => dispatch(closeStarredMessages()),
toggleStarRequest: message => dispatch(toggleStarRequest(message))
})
)
export default class StarredMessagesView extends React.PureComponent {
static propTypes = {
navigation: PropTypes.object,
messages: PropTypes.array,
user: PropTypes.object,
baseUrl: PropTypes.string,
openStarredMessages: PropTypes.func,
closeStarredMessages: PropTypes.func,
toggleStarRequest: PropTypes.func
}
constructor(props) {
super(props);
this.state = {
message: {}
};
}
componentWillMount() {
this.props.openStarredMessages(this.props.navigation.state.params.rid);
}
componentWillUnmount() {
this.props.closeStarredMessages();
}
onLongPress = (message) => {
this.setState({ message });
this.actionSheet.show();
}
handleActionPress = (actionIndex) => {
switch (actionIndex) {
case STAR_INDEX:
this.props.toggleStarRequest(this.state.message);
break;
default:
break;
}
}
renderEmpty = () => (
<View style={styles.listEmptyContainer}>
<Text>No starred messages</Text>
</View>
)
renderItem = ({ item }) => (
<Message
item={item}
style={styles.message}
reactions={item.reactions}
user={this.props.user}
baseUrl={this.props.baseUrl}
Message_TimeFormat='MMMM Do YYYY, h:mm:ss a'
onLongPress={this.onLongPress}
/>
)
render() {
if (this.props.messages.length === 0) {
return this.renderEmpty();
}
return (
[
<FlatList
key='starred-messages-view-list'
data={this.props.messages}
renderItem={this.renderItem}
style={styles.list}
keyExtractor={item => item._id}
/>,
<ActionSheet
key='starred-messages-view-action-sheet'
ref={o => this.actionSheet = o}
title='Actions'
options={options}
cancelButtonIndex={CANCEL_INDEX}
onPress={this.handleActionPress}
/>
]
);
}
}

View File

@ -0,0 +1,17 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
list: {
flex: 1,
backgroundColor: '#ffffff'
},
message: {
transform: [{ scaleY: 1 }]
},
listEmptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff'
}
});