diff --git a/.circleci/config.yml b/.circleci/config.yml index df571e695..927bbc008 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,8 +41,8 @@ jobs: - image: circleci/android:api-26-alpha environment: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError" - JVM_OPTS: -Xmx2048m + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError" + JVM_OPTS: -Xmx4096m TERM: dumb BASH_ENV: "~/.nvm/nvm.sh" diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 2422a207a..bb737dac1 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -89,6 +89,8 @@ export const SERVER = createRequestTypes('SERVER', [ export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT', 'DISCONNECT_BY_USER']); export const LOGOUT = 'LOGOUT'; // logout is always success 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 DECREMENT = 'DECREMENT'; diff --git a/app/actions/pinnedMessages.js b/app/actions/pinnedMessages.js new file mode 100644 index 000000000..a8a8f228e --- /dev/null +++ b/app/actions/pinnedMessages.js @@ -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 + }; +} diff --git a/app/actions/starredMessages.js b/app/actions/starredMessages.js new file mode 100644 index 000000000..d050d84c1 --- /dev/null +++ b/app/actions/starredMessages.js @@ -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 + }; +} diff --git a/app/containers/message/index.js b/app/containers/message/index.js index a361ef57c..f6ce250aa 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -1,6 +1,6 @@ import React from 'react'; 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 Icon from 'react-native-vector-icons/MaterialIcons'; import moment from 'moment'; @@ -33,17 +33,18 @@ import styles from './styles'; export default class Message extends React.Component { static propTypes = { item: PropTypes.object.isRequired, - reactions: PropTypes.object.isRequired, + reactions: PropTypes.any.isRequired, baseUrl: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired, message: PropTypes.object.isRequired, user: PropTypes.object.isRequired, editing: PropTypes.bool, - actionsShow: PropTypes.func, errorActionsShow: PropTypes.func, customEmojis: PropTypes.object, toggleReactionPicker: PropTypes.func, - onReactionPress: PropTypes.func + onReactionPress: PropTypes.func, + style: ViewPropTypes.style, + onLongPress: PropTypes.func } constructor(props) { @@ -73,7 +74,7 @@ export default class Message extends React.Component { } onLongPress() { - this.props.actionsShow(this.parseMessage()); + this.props.onLongPress(this.parseMessage()); } onErrorPress() { @@ -222,7 +223,7 @@ export default class Message extends React.Component { render() { const { - item, message, editing, baseUrl, customEmojis + item, message, editing, baseUrl, customEmojis, style } = this.props; const username = item.alias || item.u.username; const isEditing = message._id === item._id && editing; @@ -235,7 +236,7 @@ export default class Message extends React.Component { disabled={this.isDeleted() || this.hasError()} underlayColor='#FFFFFF' activeOpacity={0.3} - style={[styles.message, isEditing ? styles.editing : null]} + style={[styles.message, isEditing ? styles.editing : null, style]} accessibilityLabel={accessibilityLabel} > diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js index 15bbde567..2a166f424 100644 --- a/app/containers/routes/AuthRoutes.js +++ b/app/containers/routes/AuthRoutes.js @@ -4,9 +4,12 @@ import { StackNavigator, DrawerNavigator } from 'react-navigation'; import Sidebar from '../../containers/Sidebar'; import RoomsListView from '../../views/RoomsListView'; import RoomView from '../../views/RoomView'; +import RoomActionsView from '../../views/RoomActionsView'; import CreateChannelView from '../../views/CreateChannelView'; import SelectUsersView from '../../views/SelectUsersView'; import NewServerView from '../../views/NewServerView'; +import StarredMessagesView from '../../views/StarredMessagesView'; +import PinnedMessagesView from '../../views/PinnedMessagesView'; const AuthRoutes = StackNavigator( { @@ -33,6 +36,27 @@ const AuthRoutes = StackNavigator( navigationOptions: { 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' + } } }, { diff --git a/app/lib/createStore.js b/app/lib/createStore.js index b8d933e98..27f75b927 100644 --- a/app/lib/createStore.js +++ b/app/lib/createStore.js @@ -1,4 +1,4 @@ -import { createStore, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware, compose } from 'redux'; import createSagaMiddleware from 'redux-saga'; import logger from 'redux-logger'; import { composeWithDevTools } from 'remote-redux-devtools'; @@ -13,14 +13,15 @@ if (__DEV__) { /* eslint-disable global-require */ const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default(); - enhacers = composeWithDevTools( + const devComposer = composeWithDevTools({ hostname: 'localhost', port: 8000 }); + enhacers = devComposer( applyAppStateListener(), applyMiddleware(reduxImmutableStateInvariant), applyMiddleware(sagaMiddleware), applyMiddleware(logger) ); } else { - enhacers = composeWithDevTools( + enhacers = compose( applyAppStateListener(), applyMiddleware(sagaMiddleware) ); diff --git a/app/lib/realm.js b/app/lib/realm.js index bca5a7a11..ea1338bda 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -82,6 +82,7 @@ const subscriptionSchema = { // groupMentions: 0, roomUpdatedAt: { type: 'date', optional: true }, ro: { type: 'bool', optional: true }, + lastOpen: { type: 'date', optional: true }, lastMessage: { type: 'messages', optional: true } } }; diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index d2c0d9403..0a4ac8a1a 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -13,6 +13,8 @@ import { someoneTyping, roomMessageReceived } from '../actions/room'; import { setUser } from '../actions/login'; import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect'; import { requestActiveUser } from '../actions/activeUsers'; +import { starredMessageReceived, starredMessageUnstarred } from '../actions/starredMessages'; +import { pinnedMessageReceived, pinnedMessageUnpinned } from '../actions/pinnedMessages'; import Ddp from './ddp'; 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); }, @@ -272,9 +298,7 @@ const RocketChat = { _buildMessage(message) { message.status = messagesStatus.SENT; normalizeMessage(message); - if (message.urls) { - message.urls = RocketChat._parseUrls(message.urls); - } + message.urls = message.urls ? RocketChat._parseUrls(message.urls) : []; // loadHistory returns message.starred as object // stream-room-messages returns message.starred as an array message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred); @@ -358,8 +382,15 @@ const RocketChat = { createDirectMessage(username) { return call('createDirectMessage', username); }, - readMessages(rid) { - return call('readMessages', rid); + async 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) { return call('joinRoom', rid); @@ -610,6 +641,9 @@ const RocketChat = { }, setReaction(emoji, messageId) { return call('setReaction', emoji, messageId); + }, + toggleFavorite(rid, f) { + return call('toggleFavorite', rid, !f); } }; diff --git a/app/reducers/index.js b/app/reducers/index.js index f7872a539..f9a53f2a6 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -12,6 +12,8 @@ import app from './app'; import permissions from './permissions'; import customEmojis from './customEmojis'; import activeUsers from './activeUsers'; +import starredMessages from './starredMessages'; +import pinnedMessages from './pinnedMessages'; export default combineReducers({ settings, @@ -26,5 +28,7 @@ export default combineReducers({ rooms, permissions, customEmojis, - activeUsers + activeUsers, + starredMessages, + pinnedMessages }); diff --git a/app/reducers/pinnedMessages.js b/app/reducers/pinnedMessages.js new file mode 100644 index 000000000..7b9e1460a --- /dev/null +++ b/app/reducers/pinnedMessages.js @@ -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; + } +} diff --git a/app/reducers/starredMessages.js b/app/reducers/starredMessages.js new file mode 100644 index 000000000..8132a7d45 --- /dev/null +++ b/app/reducers/starredMessages.js @@ -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; + } +} diff --git a/app/sagas/index.js b/app/sagas/index.js index a27f59a54..2c24f7a84 100644 --- a/app/sagas/index.js +++ b/app/sagas/index.js @@ -9,6 +9,8 @@ import createChannel from './createChannel'; import init from './init'; import state from './state'; import activeUsers from './activeUsers'; +import starredMessages from './starredMessages'; +import pinnedMessages from './pinnedMessages'; const root = function* root() { yield all([ @@ -21,7 +23,9 @@ const root = function* root() { messages(), selectServer(), state(), - activeUsers() + activeUsers(), + starredMessages(), + pinnedMessages() ]); }; diff --git a/app/sagas/pinnedMessages.js b/app/sagas/pinnedMessages.js new file mode 100644 index 000000000..95020a1a3 --- /dev/null +++ b/app/sagas/pinnedMessages.js @@ -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; diff --git a/app/sagas/starredMessages.js b/app/sagas/starredMessages.js new file mode 100644 index 000000000..4d07d65ed --- /dev/null +++ b/app/sagas/starredMessages.js @@ -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; diff --git a/app/views/PinnedMessagesView/index.js b/app/views/PinnedMessagesView/index.js new file mode 100644 index 000000000..d737b3af7 --- /dev/null +++ b/app/views/PinnedMessagesView/index.js @@ -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 = () => ( + + No pinned messages + + ) + + renderItem = ({ item }) => ( + + ) + + render() { + if (this.props.messages.length === 0) { + return this.renderEmpty(); + } + return ( + [ + item._id} + />, + this.actionSheet = o} + title='Actions' + options={options} + cancelButtonIndex={CANCEL_INDEX} + onPress={this.handleActionPress} + /> + ] + ); + } +} diff --git a/app/views/PinnedMessagesView/styles.js b/app/views/PinnedMessagesView/styles.js new file mode 100644 index 000000000..33a5e8d11 --- /dev/null +++ b/app/views/PinnedMessagesView/styles.js @@ -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' + } +}); diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js new file mode 100644 index 000000000..098bf3469 --- /dev/null +++ b/app/views/RoomActionsView/index.js @@ -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([ + , + + {this.state.room.fname} + @{this.state.room.name} + , + + ], item) + + renderTouchableItem = (subview, item) => ( + item.route && this.props.navigation.navigate(item.route, item.params)} + underlayColor='#FFFFFF' + activeOpacity={0.5} + accessibilityLabel={item.name} + accessibilityTraits='button' + > + + {subview} + + + ) + + renderItem = ({ item }) => { + if (item.type === 'danger') { + const subview = [ + , + { item.name } + ]; + return this.renderTouchableItem(subview, item); + } + const subview = [ + , + { item.name }, + item.description && { item.description }, + + ]; + return this.renderTouchableItem(subview, item); + } + + renderSectionSeparator = (data) => { + if (!data.trailingItem) { + if (!data.trailingSection) { + return ; + } + return null; + } + return ( + + ); + } + + render() { + return ( + index} + /> + ); + } +} diff --git a/app/views/RoomActionsView/styles.js b/app/views/RoomActionsView/styles.js new file mode 100644 index 000000000..dba0c3b93 --- /dev/null +++ b/app/views/RoomActionsView/styles.js @@ -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' + } +}); diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js index 1402712f4..adb8f8d76 100644 --- a/app/views/RoomView/Header/index.js +++ b/app/views/RoomView/Header/index.js @@ -5,11 +5,14 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { HeaderBackButton } from 'react-navigation'; +import RocketChat from '../../../lib/rocketchat'; import realm from '../../../lib/realm'; import Avatar from '../../../containers/Avatar'; import { STATUS_COLORS } from '../../../constants/colors'; import styles from './styles'; import { closeRoom } from '../../../actions/room'; +import Touch from '../../../utils/touch'; + @connect(state => ({ user: state.login.user, @@ -108,9 +111,25 @@ export default class RoomHeaderView extends React.PureComponent { renderRight = () => ( + RocketChat.toggleFavorite(this.room[0].rid, this.room[0].f)} + accessibilityLabel='Star room' + accessibilityTraits='button' + underlayColor='#FFFFFF' + activeOpacity={0.5} + > + + + + {}} + onPress={() => this.props.navigation.navigate('RoomActions', { rid: this.room[0].rid })} accessibilityLabel='Room actions' accessibilityTraits='button' > diff --git a/app/views/RoomView/Header/styles.js b/app/views/RoomView/Header/styles.js index 794dc83ea..c6439e2b4 100644 --- a/app/views/RoomView/Header/styles.js +++ b/app/views/RoomView/Header/styles.js @@ -42,7 +42,7 @@ export default StyleSheet.create({ headerButton: { backgroundColor: 'transparent', height: 44, - width: 44, + width: 40, alignItems: 'center', justifyContent: 'center' } diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index ee0594d34..974112c8c 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -8,7 +8,7 @@ import equal from 'deep-equal'; import { List } from './ListView'; import * as actions from '../../actions'; 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 RocketChat from '../../lib/rocketchat'; import Message from '../../containers/message'; @@ -35,7 +35,8 @@ import styles from './styles'; openRoom: room => dispatch(openRoom(room)), editCancel: () => dispatch(editCancel()), 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 { @@ -51,8 +52,9 @@ export default class RoomView extends React.Component { Message_TimeFormat: PropTypes.string, loading: PropTypes.bool, actionMessage: PropTypes.object, - toggleReactionPicker: PropTypes.func.isRequired - // layoutAnimation: PropTypes.instanceOf(Date) + toggleReactionPicker: PropTypes.func.isRequired, + // layoutAnimation: PropTypes.instanceOf(Date), + actionsShow: PropTypes.func }; static navigationOptions = ({ navigation }) => ({ @@ -124,6 +126,10 @@ export default class RoomView extends React.Component { }); } + onMessageLongPress = (message) => { + this.props.actionsShow(message); + } + onReactionPress = (shortname, messageId) => { if (!messageId) { RocketChat.setReaction(shortname, this.props.actionMessage._id); @@ -158,6 +164,7 @@ export default class RoomView extends React.Component { Message_TimeFormat={this.props.Message_TimeFormat} user={this.props.user} onReactionPress={this.onReactionPress} + onLongPress={this.onMessageLongPress} /> ); diff --git a/app/views/StarredMessagesView/index.js b/app/views/StarredMessagesView/index.js new file mode 100644 index 000000000..c1fd7d578 --- /dev/null +++ b/app/views/StarredMessagesView/index.js @@ -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 = () => ( + + No starred messages + + ) + + renderItem = ({ item }) => ( + + ) + + render() { + if (this.props.messages.length === 0) { + return this.renderEmpty(); + } + return ( + [ + item._id} + />, + this.actionSheet = o} + title='Actions' + options={options} + cancelButtonIndex={CANCEL_INDEX} + onPress={this.handleActionPress} + /> + ] + ); + } +} diff --git a/app/views/StarredMessagesView/styles.js b/app/views/StarredMessagesView/styles.js new file mode 100644 index 000000000..33a5e8d11 --- /dev/null +++ b/app/views/StarredMessagesView/styles.js @@ -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' + } +});