Merge branch 'develop' of github.com:RocketChat/Rocket.Chat.ReactNative into develop

This commit is contained in:
Guilherme Gazzo 2018-03-29 15:27:38 -03:00
commit 7ab759aa27
No known key found for this signature in database
GPG Key ID: 1F85C9AD922D0829
33 changed files with 1170 additions and 108 deletions

View File

@ -39,6 +39,7 @@ export const ROOM = createRequestTypes('ROOM', [
'OPEN',
'CLOSE',
'LEAVE',
'ERASE',
'USER_TYPING',
'MESSAGE_RECEIVED',
'SET_LAST_OPEN',
@ -93,7 +94,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 ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET']);
export const ROLES = createRequestTypes('ROLES', ['SET']);
export const STARRED_MESSAGES = createRequestTypes('STARRED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED', 'MESSAGE_UNSTARRED']);
export const PINNED_MESSAGES = createRequestTypes('PINNED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED', 'MESSAGE_UNPINNED']);
export const MENTIONED_MESSAGES = createRequestTypes('MENTIONED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED']);

View File

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

8
app/actions/roles.js Normal file
View File

@ -0,0 +1,8 @@
import * as types from './actionsTypes';
export function setRoles(data) {
return {
type: types.ROLES.SET,
data
};
}

View File

@ -42,6 +42,13 @@ export function leaveRoom(rid) {
};
}
export function eraseRoom(rid) {
return {
type: types.ROOM.ERASE,
rid
};
}
export function userTyping(status = true) {
return {
type: types.ROOM.USER_TYPING,

View File

@ -1,8 +1,9 @@
export const AVATAR_COLORS = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B'];
export const ESLINT_FIX = null;
export const COLOR_DANGER = '#f5455c';
export const STATUS_COLORS = {
online: '#2de0a5',
busy: '#f5455c',
busy: COLOR_DANGER,
away: '#ffd21f',
offline: '#cbced1'
};

View File

@ -17,8 +17,8 @@ import {
toggleReactionPicker
} from '../actions/messages';
import { showToast } from '../utils/info';
import RocketChat from '../lib/rocketchat';
const returnAnArray = obj => obj || [];
@connect(
state => ({
showActions: state.messages.showActions,
@ -79,10 +79,6 @@ export default class MessageActions extends React.Component {
};
this.handleActionPress = this.handleActionPress.bind(this);
this.options = [''];
const { roles } = this.props.room;
const roomRoles = Array.from(Object.keys(roles), i => roles[i].value);
const userRoles = this.props.user.roles || [];
this.mergedRoles = [...new Set([...roomRoles, ...userRoles])];
this.setPermissions(this.props.permissions);
}
@ -127,7 +123,7 @@ export default class MessageActions extends React.Component {
this.PIN_INDEX = this.options.length - 1;
}
// Reaction
if (!this.isRoomReadOnly()) {
if (!this.isRoomReadOnly() || this.canReactWhenReadOnly()) {
this.options.push('Add Reaction');
this.REACTION_INDEX = this.options.length - 1;
}
@ -171,19 +167,20 @@ export default class MessageActions extends React.Component {
this.setPermissions(this.props.permissions);
}
setPermissions(permissions) {
this.hasEditPermission = returnAnArray(permissions['edit-message'])
.some(item => this.mergedRoles.indexOf(item) !== -1);
this.hasDeletePermission = returnAnArray(permissions['delete-message'])
.some(item => this.mergedRoles.indexOf(item) !== -1);
this.hasForceDeletePermission = returnAnArray(permissions['force-delete-message'])
.some(item => this.mergedRoles.indexOf(item) !== -1);
setPermissions() {
const permissions = ['edit-message', 'delete-message', 'force-delete-message'];
const result = RocketChat.hasPermission(permissions, this.props.room.rid);
this.hasEditPermission = result[permissions[0]];
this.hasDeletePermission = result[permissions[1]];
this.hasForceDeletePermission = result[permissions[2]];
}
isOwn = props => props.actionMessage.u && props.actionMessage.u._id === props.user.id;
isRoomReadOnly = () => this.props.room.ro;
canReactWhenReadOnly = () => this.props.room.reactWhenReadOnly;
allowEdit = (props) => {
if (this.isRoomReadOnly()) {
return false;

View File

@ -489,6 +489,7 @@ export default class MessageBox extends React.PureComponent {
ref={component => this.component = component}
style={styles.textBoxInput}
returnKeyType='default'
keyboardType='twitter'
blurOnSubmit={false}
placeholder='New Message'
onChangeText={text => this.onChangeText(text)}

View File

@ -0,0 +1,76 @@
import React from 'react';
import { View, StyleSheet, Text, TextInput } from 'react-native';
import PropTypes from 'prop-types';
import sharedStyles from '../views/Styles';
import { COLOR_DANGER } from '../constants/colors';
const styles = StyleSheet.create({
inputContainer: {
marginBottom: 20
},
label: {
marginBottom: 4,
fontSize: 16
},
input: {
paddingTop: 12,
paddingBottom: 12,
paddingHorizontal: 10,
borderWidth: 2,
borderRadius: 2,
backgroundColor: 'white',
borderColor: 'rgba(0,0,0,.15)',
color: 'black'
},
labelError: {
color: COLOR_DANGER
},
inputError: {
color: COLOR_DANGER,
borderColor: COLOR_DANGER
}
});
export default class RCTextInput extends React.PureComponent {
static propTypes = {
label: PropTypes.string,
value: PropTypes.string,
error: PropTypes.object,
inputProps: PropTypes.object,
inputRef: PropTypes.func,
onChangeText: PropTypes.func,
onSubmitEditing: PropTypes.func
}
static defaultProps = {
label: 'Label',
error: {}
}
render() {
const {
label, value, error, inputRef, onChangeText, onSubmitEditing, inputProps
} = this.props;
return (
<View style={styles.inputContainer}>
<Text style={[styles.label, error.error && styles.labelError]}>
{label}
</Text>
<TextInput
ref={inputRef}
style={[styles.input, error.error && styles.inputError]}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
value={value}
autoCorrect={false}
returnKeyType='next'
autoCapitalize='none'
underlineColorAndroid='transparent'
{...inputProps}
/>
{error.error && <Text style={sharedStyles.error}>{error.reason}</Text>}
</View>
);
}
}

View File

@ -46,12 +46,14 @@ export default class Message extends React.Component {
onReactionPress: PropTypes.func,
style: ViewPropTypes.style,
onLongPress: PropTypes.func,
_updatedAt: PropTypes.instanceOf(Date)
_updatedAt: PropTypes.instanceOf(Date),
archived: PropTypes.bool
}
static defaultProps = {
onLongPress: () => {},
_updatedAt: new Date()
_updatedAt: new Date(),
archived: false
}
constructor(props) {
@ -121,6 +123,14 @@ export default class Message extends React.Component {
message = `${ msg } was set ${ role } by ${ u.username }`;
} else if (t === 'subscription-role-removed') {
message = `${ msg } is no longer ${ role } by ${ u.username }`;
} else if (t === 'room_changed_description') {
message = `Room description changed to: ${ msg } by ${ u.username }`;
} else if (t === 'room_changed_announcement') {
message = `Room announcement changed to: ${ msg } by ${ u.username }`;
} else if (t === 'room_changed_topic') {
message = `Room topic changed to: ${ msg } by ${ u.username }`;
} else if (t === 'room_changed_privacy') {
message = `Room type changed to: ${ msg } by ${ u.username }`;
}
return message;
@ -130,7 +140,21 @@ export default class Message extends React.Component {
isInfoMessage() {
return [
'r', 'au', 'ru', 'ul', 'uj', 'rm', 'user-muted', 'user-unmuted', 'message_pinned', 'subscription-role-added', 'subscription-role-removed'
'r',
'au',
'ru',
'ul',
'uj',
'rm',
'user-muted',
'user-unmuted',
'message_pinned',
'subscription-role-added',
'subscription-role-removed',
'room_changed_description',
'room_changed_announcement',
'room_changed_topic',
'room_changed_privacy'
].includes(this.props.item.t);
}
@ -236,7 +260,7 @@ export default class Message extends React.Component {
render() {
const {
item, message, editing, baseUrl, customEmojis, style
item, message, editing, baseUrl, customEmojis, style, archived
} = this.props;
const username = item.alias || item.u.username;
const isEditing = message._id === item._id && editing;
@ -246,7 +270,7 @@ export default class Message extends React.Component {
<TouchableHighlight
onPress={() => this.onPress()}
onLongPress={() => this.onLongPress()}
disabled={this.isDeleted() || this.hasError()}
disabled={this.isDeleted() || this.hasError() || archived}
underlayColor='#FFFFFF'
activeOpacity={0.3}
style={[styles.message, isEditing ? styles.editing : null, style]}

View File

@ -14,6 +14,8 @@ import MentionedMessagesView from '../../views/MentionedMessagesView';
import SnippetedMessagesView from '../../views/SnippetedMessagesView';
import RoomFilesView from '../../views/RoomFilesView';
import RoomMembersView from '../../views/RoomMembersView';
import RoomInfoView from '../../views/RoomInfoView';
import RoomInfoEditView from '../../views/RoomInfoEditView';
const AuthRoutes = StackNavigator(
{
@ -89,6 +91,20 @@ const AuthRoutes = StackNavigator(
title: 'Room Members',
headerTintColor: '#292E35'
}
},
RoomInfo: {
screen: RoomInfoView,
navigationOptions: {
title: 'Room Info',
headerTintColor: '#292E35'
}
},
RoomInfoEdit: {
screen: RoomInfoEditView,
navigationOptions: {
title: 'Room Info Edit',
headerTintColor: '#292E35'
}
}
},
{

View File

@ -25,12 +25,12 @@ export default class Status extends React.Component {
shouldComponentUpdate(nextProps) {
const userId = this.props.id;
return this.status !== nextProps.activeUsers[userId];
return (nextProps.activeUsers[userId] && nextProps.activeUsers[userId].status) !== this.status;
}
get status() {
const userId = this.props.id;
return (this.props.activeUsers && this.props.activeUsers[userId]) || 'offline';
return (this.props.activeUsers && this.props.activeUsers[userId] && this.props.activeUsers[userId].status) || 'offline';
}
render() {

View File

@ -80,8 +80,6 @@ const subscriptionSchema = {
roles: { type: 'list', objectType: 'subscriptionRolesSchema' },
unread: { type: 'int', optional: true },
userMentions: { type: 'int', optional: true },
// userMentions: 0,
// groupMentions: 0,
roomUpdatedAt: { type: 'date', optional: true },
ro: { type: 'bool', optional: true },
lastOpen: { type: 'date', optional: true },
@ -89,7 +87,10 @@ const subscriptionSchema = {
description: { type: 'string', optional: true },
announcement: { type: 'string', optional: true },
topic: { type: 'string', optional: true },
blocked: { type: 'bool', optional: true }
blocked: { type: 'bool', optional: true },
reactWhenReadOnly: { type: 'bool', optional: true },
archived: { type: 'bool', optional: true },
joinCodeRequired: { type: 'bool', optional: true }
}
};
@ -237,6 +238,15 @@ const customEmojisSchema = {
}
};
const rolesSchema = {
name: 'roles',
primaryKey: '_id',
properties: {
_id: 'string',
description: { type: 'string', optional: true }
}
};
const schema = [
settingsSchema,
subscriptionSchema,
@ -254,7 +264,8 @@ const schema = [
customEmojiAliasesSchema,
customEmojisSchema,
messagesReactionsSchema,
messagesReactionsUsernamesSchema
messagesReactionsUsernamesSchema,
rolesSchema
];
class DB {
databases = {

View File

@ -12,12 +12,13 @@ import * as actions from '../actions';
import { someoneTyping, roomMessageReceived } from '../actions/room';
import { setUser, setLoginServices, removeLoginServices } from '../actions/login';
import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect';
import { requestActiveUser } from '../actions/activeUsers';
import { setActiveUser } from '../actions/activeUsers';
import { starredMessagesReceived, starredMessageUnstarred } from '../actions/starredMessages';
import { pinnedMessagesReceived, pinnedMessageUnpinned } from '../actions/pinnedMessages';
import { mentionedMessagesReceived } from '../actions/mentionedMessages';
import { snippetedMessagesReceived } from '../actions/snippetedMessages';
import { roomFilesReceived } from '../actions/roomFiles';
import { setRoles } from '../actions/roles';
import Ddp from './ddp';
export { Accounts } from 'react-native-meteor';
@ -26,6 +27,7 @@ const call = (method, ...params) => RocketChat.ddp.call(method, ...params); // e
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SERVER_TIMEOUT = 30000;
const returnAnArray = obj => obj || [];
const normalizeMessage = (lastMessage) => {
if (lastMessage) {
@ -91,13 +93,12 @@ const RocketChat = {
this._setUserTimer = null;
}
this._setUserTimer = setTimeout(() => {
reduxStore.dispatch(requestActiveUser(this.activeUsers));
reduxStore.dispatch(setActiveUser(this.activeUsers));
this._setUserTimer = null;
return this.activeUsers = {};
}, 5000);
this.activeUsers[ddpMessage.id] = status;
}, 3000);
this.activeUsers[ddpMessage.id] = ddpMessage.fields;
},
reconnect() {
if (this.ddp) {
@ -124,6 +125,8 @@ const RocketChat = {
RocketChat.getSettings();
RocketChat.getPermissions();
RocketChat.getCustomEmoji();
this.ddp.subscribe('activeUsers');
this.ddp.subscribe('roles');
});
this.ddp.on('error', (err) => {
@ -131,8 +134,6 @@ const RocketChat = {
reduxStore.dispatch(connectFailure());
});
this.ddp.on('connected', () => this.ddp.subscribe('activeUsers', null, false));
this.ddp.on('users', ddpMessage => RocketChat._setUser(ddpMessage));
this.ddp.on('stream-room-messages', (ddpMessage) => {
@ -171,6 +172,12 @@ const RocketChat = {
sub.roomUpdatedAt = data._updatedAt;
sub.lastMessage = normalizeMessage(data.lastMessage);
sub.ro = data.ro;
sub.description = data.description;
sub.topic = data.topic;
sub.announcement = data.announcement;
sub.reactWhenReadOnly = data.reactWhenReadOnly;
sub.archived = data.archived;
sub.joinCodeRequired = data.joinCodeRequired;
});
}
});
@ -334,6 +341,28 @@ const RocketChat = {
this.loginServiceTimer = setTimeout(() => reduxStore.dispatch(removeLoginServices()), 1000);
}
});
this.ddp.on('rocketchat_roles', (ddpMessage) => {
this.roles = this.roles || {};
if (this.roleTimer) {
clearTimeout(this.roleTimer);
this.roleTimer = null;
}
this.roleTimer = setTimeout(() => {
reduxStore.dispatch(setRoles(this.roles));
database.write(() => {
_.forEach(this.roles, (description, _id) => {
database.create('roles', { _id, description }, true);
});
});
this.roleTimer = null;
return this.roles = {};
}, 5000);
this.roles[ddpMessage.id] = ddpMessage.fields.description;
});
}).catch(console.log);
},
@ -649,6 +678,9 @@ const RocketChat = {
subscription.description = room.description;
subscription.topic = room.topic;
subscription.announcement = room.announcement;
subscription.reactWhenReadOnly = room.reactWhenReadOnly;
subscription.archived = room.archived;
subscription.joinCodeRequired = room.joinCodeRequired;
}
if (subscription.roles) {
subscription.roles = subscription.roles.map(role => ({ value: role }));
@ -823,6 +855,17 @@ const RocketChat = {
getRoomMembers(rid, allUsers) {
return call('getUsersOfRoom', rid, allUsers);
},
getUserRoles() {
return call('getUserRoles');
},
async getRoomMember(rid, currentUserId) {
try {
const membersResult = await RocketChat.getRoomMembers(rid, true);
return Promise.resolve(membersResult.records.find(m => m.id !== currentUserId));
} catch (error) {
return Promise.reject(error);
}
},
toggleBlockUser(rid, blocked, block) {
if (block) {
return call('blockUser', { rid, blocked });
@ -831,6 +874,40 @@ const RocketChat = {
},
leaveRoom(rid) {
return call('leaveRoom', rid);
},
eraseRoom(rid) {
return call('eraseRoom', rid);
},
toggleArchiveRoom(rid, archive) {
if (archive) {
return call('archiveRoom', rid);
}
return call('unarchiveRoom', rid);
},
saveRoomSettings(rid, params) {
return call('saveRoomSettings', rid, params);
},
hasPermission(permissions, rid) {
// get the room from realm
const room = database.objects('subscriptions').filtered('rid = $0', rid)[0];
// get room roles
const { roles } = room;
// transform room roles to array
const roomRoles = Array.from(Object.keys(roles), i => roles[i].value);
// get user roles on the server from redux
const userRoles = reduxStore.getState().login.user.roles || [];
// get all permissions from redux
const allPermissions = reduxStore.getState().permissions;
// merge both roles
const mergedRoles = [...new Set([...roomRoles, ...userRoles])];
// return permissions in object format
// e.g. { 'edit-room': true, 'set-readonly': false }
return permissions.reduce((result, permission) => {
result[permission] = returnAnArray(allPermissions[permission])
.some(item => mergedRoles.indexOf(item) !== -1);
return result;
}, {});
}
};

View File

@ -12,6 +12,7 @@ import app from './app';
import permissions from './permissions';
import customEmojis from './customEmojis';
import activeUsers from './activeUsers';
import roles from './roles';
import starredMessages from './starredMessages';
import pinnedMessages from './pinnedMessages';
import mentionedMessages from './mentionedMessages';
@ -32,6 +33,7 @@ export default combineReducers({
permissions,
customEmojis,
activeUsers,
roles,
starredMessages,
pinnedMessages,
mentionedMessages,

15
app/reducers/roles.js Normal file
View File

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

View File

@ -1,13 +0,0 @@
import { put, takeLatest } from 'redux-saga/effects';
import * as types from '../actions/actionsTypes';
import { setActiveUser } from '../actions/activeUsers';
const watchActiveUsers = function* handleInput({ users }) {
yield put(setActiveUser(users));
};
const root = function* root() {
yield takeLatest(types.ACTIVE_USERS.REQUEST, watchActiveUsers);
};
export default root;

View File

@ -8,7 +8,6 @@ import selectServer from './selectServer';
import createChannel from './createChannel';
import init from './init';
import state from './state';
import activeUsers from './activeUsers';
import starredMessages from './starredMessages';
import pinnedMessages from './pinnedMessages';
import mentionedMessages from './mentionedMessages';
@ -26,7 +25,6 @@ const root = function* root() {
messages(),
selectServer(),
state(),
activeUsers(),
starredMessages(),
pinnedMessages(),
mentionedMessages(),

View File

@ -4,6 +4,7 @@ import * as actions from '../actions';
import { setServer } from '../actions/server';
import { restoreToken } from '../actions/login';
import { APP } from '../actions/actionsTypes';
import { setRoles } from '../actions/roles';
import database from '../lib/realm';
import RocketChat from '../lib/rocketchat';
@ -23,6 +24,11 @@ const restore = function* restore() {
yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length))));
const emojis = database.objects('customEmojis');
yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length))));
const roles = database.objects('roles');
yield put(setRoles(roles.reduce((result, role) => {
result[role._id] = role.description;
return result;
}, {})));
}
yield put(actions.appReady({}));
} catch (e) {

View File

@ -11,6 +11,7 @@ import database from '../lib/realm';
import * as NavigationService from '../containers/routes/NavigationService';
const leaveRoom = rid => RocketChat.leaveRoom(rid);
const eraseRoom = rid => RocketChat.eraseRoom(rid);
const getRooms = function* getRooms() {
return yield RocketChat.getRooms();
@ -121,9 +122,7 @@ const updateLastOpen = function* updateLastOpen() {
yield put(setLastOpen());
};
const handleLeaveRoom = function* handleLeaveRoom({ rid }) {
try {
yield call(leaveRoom, rid);
const goRoomsListAndDelete = function* goRoomsListAndDelete(rid) {
NavigationService.goRoomsList();
yield delay(1000);
database.write(() => {
@ -132,15 +131,30 @@ const handleLeaveRoom = function* handleLeaveRoom({ rid }) {
const subscription = database.objects('subscriptions').filtered('rid = $0', rid);
database.delete(subscription);
});
};
const handleLeaveRoom = function* handleLeaveRoom({ rid }) {
try {
yield call(leaveRoom, rid);
yield goRoomsListAndDelete(rid);
} catch (e) {
if (e.error === 'error-you-are-last-owner') {
Alert.alert('You are the last owner. Please set new owner before leaving the room.');
} else {
Alert.alert(e);
Alert.alert('Something happened when leaving room!');
}
}
};
const handleEraseRoom = function* handleEraseRoom({ rid }) {
try {
yield call(eraseRoom, rid);
yield goRoomsListAndDelete(rid);
} catch (e) {
Alert.alert('Something happened when erasing room!');
}
};
const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping);
yield takeLatest(types.LOGIN.SUCCESS, watchRoomsRequest);
@ -150,5 +164,6 @@ const root = function* root() {
yield takeLatest(FOREGROUND, watchRoomsRequest);
yield takeLatest(BACKGROUND, updateLastOpen);
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
yield takeLatest(types.ROOM.ERASE, handleEraseRoom);
};
export default root;

View File

@ -6,7 +6,9 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import { connect } from 'react-redux';
import styles from './styles';
import sharedStyles from '../Styles';
import Avatar from '../../containers/Avatar';
import Status from '../../containers/status';
import Touch from '../../utils/touch';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
@ -33,13 +35,15 @@ export default class RoomActionsView extends React.PureComponent {
this.state = {
sections: [],
room: {},
members: []
members: [],
member: {}
};
}
async componentDidMount() {
await this.updateRoom();
this.updateRoomMembers();
this.updateRoomMember();
this.rooms.addListener(this.updateRoom);
}
@ -59,7 +63,7 @@ export default class RoomActionsView extends React.PureComponent {
getRoomTitle = room => (room.t === 'd' ? room.fname : room.name);
updateRoomMembers = async() => {
let members;
let members = [];
try {
const membersResult = await RocketChat.getRoomMembers(this.state.room.rid, false);
members = membersResult.records;
@ -70,6 +74,17 @@ export default class RoomActionsView extends React.PureComponent {
this.updateSections();
}
updateRoomMember = async() => {
if (this.state.room.t === 'd') {
try {
const member = await RocketChat.getRoomMember(this.state.room.rid, this.props.user.id);
this.setState({ member });
} catch (error) {
console.warn(error);
}
}
}
updateRoom = async() => {
const [room] = this.rooms;
await this.setState({ room });
@ -80,12 +95,17 @@ export default class RoomActionsView extends React.PureComponent {
const { rid, t, blocked } = this.state.room;
const { members } = this.state;
const sections = [{
data: [{ icon: 'ios-star', name: 'USER' }],
data: [{
icon: 'ios-star',
name: 'USER',
route: 'RoomInfo',
params: { rid }
}],
renderItem: this.renderRoomInfo
}, {
data: [
{ icon: 'ios-call-outline', name: 'Voice call' },
{ icon: 'ios-videocam-outline', name: 'Video call' }
{ icon: 'ios-call-outline', name: 'Voice call', disabled: true },
{ icon: 'ios-videocam-outline', name: 'Video call', disabled: true }
],
renderItem: this.renderItem
}, {
@ -108,8 +128,8 @@ export default class RoomActionsView extends React.PureComponent {
route: 'StarredMessages',
params: { rid }
},
{ icon: 'ios-search', name: 'Search' },
{ icon: 'ios-share-outline', name: 'Share' },
{ icon: 'ios-search', name: 'Search', disabled: true },
{ icon: 'ios-share-outline', name: 'Share', disabled: true },
{
icon: 'ios-pin',
name: 'Pinned',
@ -122,14 +142,14 @@ export default class RoomActionsView extends React.PureComponent {
route: 'SnippetedMessages',
params: { rid }
},
{ icon: 'ios-notifications-outline', name: 'Notifications preferences' }
{ icon: 'ios-notifications-outline', name: 'Notifications preferences', disabled: true }
],
renderItem: this.renderItem
}];
if (t === 'd') {
sections.push({
data: [
{ icon: 'ios-volume-off', name: 'Mute user' },
{ icon: 'ios-volume-off', name: 'Mute user', disabled: true },
{
icon: 'block',
name: `${ blocked ? 'Unblock' : 'Block' } user`,
@ -151,7 +171,7 @@ export default class RoomActionsView extends React.PureComponent {
}
sections.push({
data: [
{ icon: 'ios-volume-off', name: 'Mute channel' },
{ icon: 'ios-volume-off', name: 'Mute channel', disabled: true },
{
icon: 'block',
name: 'Leave channel',
@ -167,8 +187,7 @@ export default class RoomActionsView extends React.PureComponent {
toggleBlockUser = () => {
const { rid, blocked } = this.state.room;
const { members } = this.state;
const member = members.find(m => m.id !== this.props.user.id);
const { member } = this.state;
RocketChat.toggleBlockUser(rid, member._id, !blocked);
}
@ -185,16 +204,14 @@ export default class RoomActionsView extends React.PureComponent {
{
text: 'Yes, leave it!',
style: 'destructive',
onPress: async() => {
this.props.leaveRoom(room.rid);
}
onPress: () => this.props.leaveRoom(room.rid)
}
]
);
}
renderRoomInfo = ({ item }) => {
const { room } = this.state;
const { room, member } = this.state;
const { name, t, topic } = room;
return (
this.renderTouchableItem([
@ -205,12 +222,14 @@ export default class RoomActionsView extends React.PureComponent {
style={styles.avatar}
baseUrl={this.props.baseUrl}
type={t}
/>,
>
{t === 'd' ? <Status style={sharedStyles.status} id={member._id} /> : null }
</Avatar>,
<View key='name' style={styles.roomTitleContainer}>
<Text style={styles.roomTitle}>{ this.getRoomTitle(room) }</Text>
<Text style={styles.roomDescription} ellipsizeMode='tail' numberOfLines={1}>{t === 'd' ? `@${ name }` : topic}</Text>
</View>,
<Icon key='icon' name='ios-arrow-forward' size={20} style={styles.sectionItemIcon} color='#cbced1' />
<Icon key='icon' name='ios-arrow-forward' size={20} style={styles.sectionItemIcon} color='#ccc' />
], item)
);
}
@ -223,7 +242,7 @@ export default class RoomActionsView extends React.PureComponent {
accessibilityLabel={item.name}
accessibilityTraits='button'
>
<View style={styles.sectionItem}>
<View style={[styles.sectionItem, item.disabled && styles.sectionItemDisabled]}>
{subview}
</View>
</Touch>
@ -241,11 +260,13 @@ export default class RoomActionsView extends React.PureComponent {
<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' />
<Icon key='right-icon' name='ios-arrow-forward' size={20} style={styles.sectionItemIcon} color='#ccc' />
];
return this.renderTouchableItem(subview, item);
}
renderSeparator = () => <View style={styles.separator} />;
renderSectionSeparator = (data) => {
if (!data.trailingItem) {
if (!data.trailingSection) {
@ -265,6 +286,7 @@ export default class RoomActionsView extends React.PureComponent {
stickySectionHeadersEnabled={false}
sections={this.state.sections}
SectionSeparatorComponent={this.renderSectionSeparator}
ItemSeparatorComponent={this.renderSeparator}
keyExtractor={(item, index) => index}
/>
);

View File

@ -13,10 +13,13 @@ export default StyleSheet.create({
},
sectionItem: {
backgroundColor: '#ffffff',
paddingVertical: 10,
paddingVertical: 16,
flexDirection: 'row',
alignItems: 'center'
},
sectionItemDisabled: {
opacity: 0.3
},
sectionItemIcon: {
width: 45,
textAlign: 'center'
@ -25,7 +28,11 @@ export default StyleSheet.create({
flex: 1
},
sectionItemDescription: {
color: '#cbced1'
color: '#ccc'
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#ddd'
},
sectionSeparator: {
height: 10,
@ -49,6 +56,6 @@ export default StyleSheet.create({
},
roomDescription: {
fontSize: 12,
color: '#cbced1'
color: '#ccc'
}
});

View File

@ -0,0 +1,45 @@
import React from 'react';
import { View, Text, Switch } from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles';
import sharedStyles from '../../views/Styles';
export default class SwitchContainer extends React.PureComponent {
static propTypes = {
value: PropTypes.bool,
disabled: PropTypes.bool,
leftLabelPrimary: PropTypes.string,
leftLabelSecondary: PropTypes.string,
rightLabelPrimary: PropTypes.string,
rightLabelSecondary: PropTypes.string,
onValueChange: PropTypes.func
}
render() {
const {
value, disabled, onValueChange, leftLabelPrimary, leftLabelSecondary, rightLabelPrimary, rightLabelSecondary
} = this.props;
return (
[
<View key='switch-container' style={styles.switchContainer}>
<View style={[styles.switchLabelContainer, sharedStyles.alignItemsFlexEnd]}>
<Text style={styles.switchLabelPrimary}>{leftLabelPrimary}</Text>
<Text style={[styles.switchLabelSecondary, sharedStyles.textAlignRight]}>{leftLabelSecondary}</Text>
</View>
<Switch
style={styles.switch}
onValueChange={onValueChange}
value={value}
disabled={disabled}
/>
<View style={styles.switchLabelContainer}>
<Text style={styles.switchLabelPrimary}>{rightLabelPrimary}</Text>
<Text style={styles.switchLabelSecondary}>{rightLabelSecondary}</Text>
</View>
</View>,
<View key='switch-divider' style={styles.divider} />
]
);
}
}

View File

@ -0,0 +1,381 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, View, ScrollView, TouchableOpacity, SafeAreaView, Keyboard, Alert } from 'react-native';
import Spinner from 'react-native-loading-spinner-overlay';
import { connect } from 'react-redux';
import KeyboardView from '../../presentation/KeyboardView';
import sharedStyles from '../Styles';
import styles from './styles';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { showErrorAlert, showToast } from '../../utils/info';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
import { eraseRoom } from '../../actions/room';
import RCTextInput from '../../containers/TextInput';
import SwitchContainer from './SwitchContainer';
import random from '../../utils/random';
const PERMISSION_SET_READONLY = 'set-readonly';
const PERMISSION_SET_REACT_WHEN_READONLY = 'set-react-when-readonly';
const PERMISSION_ARCHIVE = 'archive-room';
const PERMISSION_UNARCHIVE = 'unarchive-room';
const PERMISSION_DELETE_C = 'delete-c';
const PERMISSION_DELETE_P = 'delete-p';
const PERMISSIONS_ARRAY = [
PERMISSION_SET_READONLY,
PERMISSION_SET_REACT_WHEN_READONLY,
PERMISSION_ARCHIVE,
PERMISSION_UNARCHIVE,
PERMISSION_DELETE_C,
PERMISSION_DELETE_P
];
@connect(null, dispatch => ({
eraseRoom: rid => dispatch(eraseRoom(rid))
}))
export default class RoomInfoEditView extends React.Component {
static propTypes = {
navigation: PropTypes.object,
eraseRoom: PropTypes.func
};
constructor(props) {
super(props);
const { rid } = props.navigation.state.params;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.permissions = {};
this.state = {
room: {},
name: '',
description: '',
topic: '',
announcement: '',
joinCode: '',
nameError: {},
saving: false,
t: false,
ro: false,
reactWhenReadOnly: false
};
}
async componentDidMount() {
await this.updateRoom();
this.init();
this.rooms.addListener(this.updateRoom);
this.permissions = RocketChat.hasPermission(PERMISSIONS_ARRAY, this.state.room.rid);
}
componentWillUnmount() {
this.rooms.removeAllListeners();
}
updateRoom = async() => {
const [room] = this.rooms;
this.setState({ room });
}
init = () => {
const {
name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired
} = this.state.room;
// fake password just to user knows about it
this.randomValue = random(15);
this.setState({
name,
description,
topic,
announcement,
t: t === 'p',
ro,
reactWhenReadOnly,
joinCode: joinCodeRequired ? this.randomValue : ''
});
}
clearErrors = () => {
this.setState({
nameError: {}
});
}
reset = () => {
this.clearErrors();
this.init();
}
formIsChanged = () => {
const {
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode
} = this.state;
return !(room.name === name &&
room.description === description &&
room.topic === topic &&
room.announcement === announcement &&
this.randomValue === joinCode &&
room.t === 'p' === t &&
room.ro === ro &&
room.reactWhenReadOnly === reactWhenReadOnly
);
}
submit = async() => {
Keyboard.dismiss();
const {
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode
} = this.state;
this.setState({ saving: true });
let error = false;
if (!this.formIsChanged()) {
showErrorAlert('Nothing to save!');
return;
}
// Clear error objects
await this.clearErrors();
const params = {};
// Name
if (room.name !== name) {
params.roomName = name;
}
// Description
if (room.description !== description) {
params.roomDescription = description;
}
// Topic
if (room.topic !== topic) {
params.roomTopic = topic;
}
// Announcement
if (room.announcement !== announcement) {
params.roomAnnouncement = announcement;
}
// Room Type
if (room.t !== t) {
params.roomType = t ? 'p' : 'c';
}
// Read Only
if (room.ro !== ro) {
params.readOnly = ro;
}
// React When Read Only
if (room.reactWhenReadOnly !== reactWhenReadOnly) {
params.reactWhenReadOnly = reactWhenReadOnly;
}
// Join Code
if (this.randomValue !== joinCode) {
params.joinCode = joinCode;
}
try {
await RocketChat.saveRoomSettings(room.rid, params);
} catch (e) {
if (e.error === 'error-invalid-room-name') {
this.setState({ nameError: e });
}
error = true;
}
await this.setState({ saving: false });
setTimeout(() => {
if (error) {
showErrorAlert('There was an error while saving settings!');
} else {
showToast('Settings succesfully changed!');
}
}, 100);
}
delete = () => {
Alert.alert(
'Are you sure?',
'Deleting a room will delete all messages posted within the room. This cannot be undone.',
[
{
text: 'Cancel',
style: 'cancel'
},
{
text: 'Yes, delete it!',
style: 'destructive',
onPress: () => this.props.eraseRoom(this.state.room.rid)
}
],
{ cancelable: false }
);
}
toggleArchive = () => {
const { archived } = this.state.room;
const action = `${ archived ? 'un' : '' }archive`;
Alert.alert(
'Are you sure?',
`Do you really want to ${ action } this room?`,
[
{
text: 'Cancel',
style: 'cancel'
},
{
text: `Yes, ${ action } it!`,
style: 'destructive',
onPress: () => {
try {
RocketChat.toggleArchiveRoom(this.state.room.rid, !archived);
} catch (error) {
alert(error);
}
}
}
],
{ cancelable: false }
);
}
hasDeletePermission = () => (
this.state.room.t === 'p' ? this.permissions[PERMISSION_DELETE_P] : this.permissions[PERMISSION_DELETE_C]
);
hasArchivePermission = () => (
this.permissions[PERMISSION_ARCHIVE] || this.permissions[PERMISSION_UNARCHIVE]
);
render() {
const {
name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode
} = this.state;
return (
<KeyboardView
contentContainerStyle={sharedStyles.container}
keyboardVerticalOffset={128}
>
<ScrollView
style={sharedStyles.loginView}
{...scrollPersistTaps}
>
<SafeAreaView>
<View style={sharedStyles.formContainer}>
<RCTextInput
inputRef={(e) => { this.name = e; }}
label='Name'
value={name}
onChangeText={value => this.setState({ name: value })}
onSubmitEditing={() => { this.description.focus(); }}
error={nameError}
/>
<RCTextInput
inputRef={(e) => { this.description = e; }}
label='Description'
value={description}
onChangeText={value => this.setState({ description: value })}
onSubmitEditing={() => { this.topic.focus(); }}
inputProps={{ multiline: true }}
/>
<RCTextInput
inputRef={(e) => { this.topic = e; }}
label='Topic'
value={topic}
onChangeText={value => this.setState({ topic: value })}
onSubmitEditing={() => { this.announcement.focus(); }}
inputProps={{ multiline: true }}
/>
<RCTextInput
inputRef={(e) => { this.announcement = e; }}
label='Announcement'
value={announcement}
onChangeText={value => this.setState({ announcement: value })}
onSubmitEditing={() => { this.joinCode.focus(); }}
inputProps={{ multiline: true }}
/>
<RCTextInput
inputRef={(e) => { this.joinCode = e; }}
label='Password'
value={joinCode}
onChangeText={value => this.setState({ joinCode: value })}
onSubmitEditing={this.submit}
inputProps={{ secureTextEntry: true }}
/>
<SwitchContainer
value={t}
leftLabelPrimary='Public'
leftLabelSecondary='Everyone can access this channel'
rightLabelPrimary='Private'
rightLabelSecondary='Just invited people can access this channel'
onValueChange={value => this.setState({ t: value })}
/>
<SwitchContainer
value={ro}
leftLabelPrimary='Colaborative'
leftLabelSecondary='All users in the channel can write new messages'
rightLabelPrimary='Read Only'
rightLabelSecondary='Only authorized users can write new messages'
onValueChange={value => this.setState({ ro: value })}
disabled={!this.permissions[PERMISSION_SET_READONLY]}
/>
{ro &&
<SwitchContainer
value={reactWhenReadOnly}
leftLabelPrimary='No Reactions'
leftLabelSecondary='Reactions are disabled'
rightLabelPrimary='Allow Reactions'
rightLabelSecondary='Reactions are enabled'
onValueChange={value => this.setState({ reactWhenReadOnly: value })}
disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
/>
}
<TouchableOpacity
style={[sharedStyles.buttonContainer, !this.formIsChanged() && styles.buttonContainerDisabled]}
onPress={this.submit}
disabled={!this.formIsChanged()}
>
<Text style={sharedStyles.button} accessibilityTraits='button'>SAVE</Text>
</TouchableOpacity>
<View style={{ flexDirection: 'row' }}>
<TouchableOpacity
style={[sharedStyles.buttonContainer_inverted, styles.buttonInverted, { flex: 1 }]}
onPress={this.reset}
>
<Text style={sharedStyles.button_inverted} accessibilityTraits='button'>RESET</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
sharedStyles.buttonContainer_inverted,
styles.buttonDanger,
!this.hasArchivePermission() && sharedStyles.opacity5,
{ flex: 1, marginLeft: 10 }
]}
onPress={this.toggleArchive}
disabled={!this.hasArchivePermission()}
>
<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>
{ room.archived ? 'UNARCHIVE' : 'ARCHIVE' }
</Text>
</TouchableOpacity>
</View>
<View style={styles.divider} />
<TouchableOpacity
style={[
sharedStyles.buttonContainer_inverted,
sharedStyles.buttonContainerLastChild,
styles.buttonDanger,
!this.hasDeletePermission() && sharedStyles.opacity5
]}
onPress={this.delete}
disabled={!this.hasDeletePermission()}
>
<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>DELETE</Text>
</TouchableOpacity>
</View>
<Spinner visible={this.state.saving} textContent='Loading...' textStyle={{ color: '#FFF' }} />
</SafeAreaView>
</ScrollView>
</KeyboardView>
);
}
}

View File

@ -0,0 +1,46 @@
import { StyleSheet } from 'react-native';
import { COLOR_DANGER } from '../../constants/colors';
export default StyleSheet.create({
buttonInverted: {
borderColor: 'rgba(0,0,0,.15)',
borderWidth: 2,
borderRadius: 2
},
buttonContainerDisabled: {
backgroundColor: 'rgba(65, 72, 82, 0.7)'
},
buttonDanger: {
borderColor: COLOR_DANGER,
borderWidth: 2,
borderRadius: 2
},
colorDanger: {
color: COLOR_DANGER
},
switchContainer: {
flexDirection: 'row',
alignItems: 'flex-start'
},
switchLabelContainer: {
flex: 1,
paddingHorizontal: 10
},
switchLabelPrimary: {
fontSize: 16,
paddingBottom: 6
},
switchLabelSecondary: {
fontSize: 12
},
switch: {
alignSelf: 'center'
},
divider: {
height: StyleSheet.hairlineWidth,
borderColor: '#ddd',
borderBottomWidth: StyleSheet.hairlineWidth,
marginVertical: 20
}
});

View File

@ -0,0 +1,192 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, ScrollView } from 'react-native';
import { connect } from 'react-redux';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment';
import Status from '../../containers/status';
import Avatar from '../../containers/Avatar';
import styles from './styles';
import sharedStyles from '../Styles';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
import Touch from '../../utils/touch';
const PERMISSION_EDIT_ROOM = 'edit-room';
const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: state.login.user,
permissions: state.permissions,
activeUsers: state.activeUsers,
Message_TimeFormat: state.settings.Message_TimeFormat,
roles: state.roles
}))
export default class RoomInfoView extends React.Component {
static propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
navigation: PropTypes.object,
activeUsers: PropTypes.object,
Message_TimeFormat: PropTypes.string,
roles: PropTypes.object
}
static navigationOptions = ({ navigation }) => {
const params = navigation.state.params || {};
if (!params.hasEditPermission) {
return;
}
return {
headerRight: (
<Touch
onPress={() => navigation.navigate('RoomInfoEdit', { rid: navigation.state.params.rid })}
underlayColor='#ffffff'
activeOpacity={0.5}
accessibilityLabel='edit'
accessibilityTraits='button'
>
<View style={styles.headerButton}>
<MaterialIcon name='edit' size={20} />
</View>
</Touch>
)
};
};
constructor(props) {
super(props);
const { rid } = props.navigation.state.params;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.sub = {
unsubscribe: () => {}
};
this.state = {
room: {},
roomUser: {},
roles: []
};
}
async componentDidMount() {
await this.updateRoom();
this.rooms.addListener(this.updateRoom);
// get user of room
if (this.state.room.t === 'd') {
try {
const roomUser = await RocketChat.getRoomMember(this.state.room.rid, this.props.user.id);
this.setState({ roomUser });
const username = this.state.room.name;
const activeUser = this.props.activeUsers[roomUser._id];
if (!activeUser || !activeUser.utcOffset) {
// get full user data looking for utcOffset
// will be catched by .on('users) and saved on activeUsers reducer
this.getFullUserData(username);
}
// get all users roles
// needs to be changed by a better method
const allUsersRoles = await RocketChat.getUserRoles();
const userRoles = allUsersRoles.find(user => user.username === username);
if (userRoles) {
this.setState({ roles: userRoles.roles || [] });
}
} catch (error) {
alert(error);
}
} else {
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], this.state.room.rid);
this.props.navigation.setParams({ hasEditPermission: permissions[PERMISSION_EDIT_ROOM] });
}
}
componentWillUnmount() {
this.rooms.removeAllListeners();
this.sub.unsubscribe();
}
getFullUserData = async(username) => {
const result = await RocketChat.subscribe('fullUserData', username);
this.sub = result;
}
getRoomTitle = room => (room.t === 'd' ? room.fname : room.name);
isDirect = () => this.state.room.t === 'd';
updateRoom = async() => {
const [room] = this.rooms;
this.setState({ room });
}
// TODO: translate
renderItem = (key, room) => (
<View style={styles.item}>
<Text style={styles.itemLabel}>{camelize(key)}</Text>
<Text style={[styles.itemContent, !room[key] && styles.itemContent__empty]}>{ room[key] ? room[key] : `No ${ key } provided.` }</Text>
</View>
);
renderRoles = () => (
this.state.roles.length > 0 &&
<View style={styles.item}>
<Text style={styles.itemLabel}>Roles</Text>
<View style={styles.rolesContainer}>
{this.state.roles.map(role => (
<View style={styles.roleBadge} key={role}>
<Text>{ this.props.roles[role] }</Text>
</View>
))}
</View>
</View>
)
renderTimezone = (userId) => {
if (this.props.activeUsers[userId]) {
const { utcOffset } = this.props.activeUsers[userId];
if (!utcOffset) {
return null;
}
// TODO: translate
return (
<View style={styles.item}>
<Text style={styles.itemLabel}>Timezone</Text>
<Text style={styles.itemContent}>{moment().utcOffset(utcOffset).format(this.props.Message_TimeFormat)} (UTC { utcOffset })</Text>
</View>
);
}
return null;
}
render() {
const { room, roomUser } = this.state;
const { name, t } = room;
return (
<ScrollView style={styles.container}>
<View style={styles.avatarContainer}>
<Avatar
text={name}
size={100}
style={styles.avatar}
baseUrl={this.props.baseUrl}
type={t}
>
{t === 'd' ? <Status style={[sharedStyles.status, styles.status]} id={roomUser._id} /> : null}
</Avatar>
<Text style={styles.roomTitle}>{ this.getRoomTitle(room) }</Text>
</View>
{!this.isDirect() && this.renderItem('description', room)}
{!this.isDirect() && this.renderItem('topic', room)}
{!this.isDirect() && this.renderItem('announcement', room)}
{this.isDirect() && this.renderRoles()}
{this.isDirect() && this.renderTimezone(roomUser._id)}
</ScrollView>
);
}
}

View File

@ -0,0 +1,70 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
backgroundColor: '#ffffff',
padding: 10
},
headerButton: {
backgroundColor: 'transparent',
height: 44,
width: 44,
alignItems: 'center',
justifyContent: 'center'
},
item: {
padding: 10,
// borderColor: '#EBEDF1',
// borderTopWidth: StyleSheet.hairlineWidth,
justifyContent: 'center'
},
avatarContainer: {
height: 250,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
avatar: {
marginHorizontal: 10
},
roomTitle: {
fontSize: 18,
paddingTop: 20
},
roomDescription: {
fontSize: 14,
color: '#ccc',
paddingTop: 10
},
status: {
borderRadius: 24,
width: 24,
height: 24,
borderWidth: 4,
bottom: -4,
right: -4
},
itemLabel: {
fontWeight: '600',
marginBottom: 10
},
itemContent: {
color: '#ccc'
},
itemContent__empty: {
fontStyle: 'italic'
},
rolesContainer: {
flexDirection: 'row',
flexWrap: 'wrap'
},
roleBadge: {
padding: 8,
backgroundColor: '#ddd',
borderRadius: 2,
marginRight: 5,
marginBottom: 5
}
});

View File

@ -4,6 +4,7 @@ import { FlatList, Text, View, TextInput } from 'react-native';
import { connect } from 'react-redux';
import styles from './styles';
import sharedStyles from '../Styles';
import Avatar from '../../containers/Avatar';
import Status from '../../containers/status';
import Touch from '../../utils/touch';
@ -115,7 +116,7 @@ export default class MentionedMessagesView extends React.PureComponent {
accessibilityTraits='button'
>
<View style={styles.item}>
<Avatar text={item.username} size={30} type='d' style={styles.avatar}>{<Status style={styles.status} id={item._id} />}</Avatar>
<Avatar text={item.username} size={30} type='d' style={styles.avatar}>{<Status style={[sharedStyles.status, styles.status]} id={item._id} />}</Avatar>
<Text style={styles.username}>{item.username}</Text>
</View>
</Touch>

View File

@ -7,7 +7,7 @@ export default StyleSheet.create({
},
item: {
flexDirection: 'row',
paddingVertical: 8,
paddingVertical: 10,
paddingHorizontal: 16,
alignItems: 'center'
},
@ -15,14 +15,12 @@ export default StyleSheet.create({
marginRight: 16
},
status: {
position: 'absolute',
bottom: -3,
right: -3,
bottom: -2,
right: -2,
borderWidth: 2,
borderColor: '#fff',
borderRadius: 12,
width: 12,
height: 12
borderRadius: 10,
width: 10,
height: 10
},
separator: {
height: StyleSheet.hairlineWidth,

View File

@ -50,7 +50,8 @@ export default class RoomHeaderView extends React.PureComponent {
getUserStatus() {
const userId = this.rid.replace(this.props.user.id, '').trim();
return this.props.activeUsers[userId] || 'offline';
const userInfo = this.props.activeUsers[userId];
return (userInfo && userInfo.status) || 'offline';
}
getUserStatusLabel() {
@ -86,7 +87,12 @@ export default class RoomHeaderView extends React.PureComponent {
}
return (
<TouchableOpacity style={styles.titleContainer} accessibilityLabel={accessibilityLabel} accessibilityTraits='header'>
<TouchableOpacity
style={styles.titleContainer}
accessibilityLabel={accessibilityLabel}
accessibilityTraits='header'
onPress={() => this.props.navigation.navigate('RoomInfo', { rid: this.rid })}
>
{this.isDirect() ?
<View style={[styles.status, { backgroundColor: STATUS_COLORS[this.getUserStatus()] }]} />
: null

View File

@ -157,6 +157,7 @@ export default class RoomView extends React.Component {
user={this.props.user}
onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
archived={this.state.room.archived}
/>
);
@ -171,7 +172,7 @@ export default class RoomView extends React.Component {
</View>
);
}
if (this.state.room.ro) {
if (this.state.room.ro || this.state.room.archived) {
return (
<View style={styles.readOnly}>
<Text>This room is read only</Text>

View File

@ -29,7 +29,6 @@ const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
login: () => dispatch(actions.login()),
connect: () => dispatch(server.connectRequest())
}))
export default class RoomsListView extends React.Component {
static propTypes = {
navigation: PropTypes.object.isRequired,
@ -51,7 +50,7 @@ export default class RoomsListView extends React.Component {
searchText: ''
};
this._keyExtractor = this._keyExtractor.bind(this);
this.data = database.objects('subscriptions').sorted('roomUpdatedAt', true);
this.data = database.objects('subscriptions').filtered('archived != true').sorted('roomUpdatedAt', true);
}
componentDidMount() {
@ -67,7 +66,7 @@ export default class RoomsListView extends React.Component {
componentWillReceiveProps(props) {
if (this.props.server !== props.server) {
this.data.removeListener(this.updateState);
this.data = database.objects('subscriptions').sorted('roomUpdatedAt', true);
this.data = database.objects('subscriptions').filtered('archived != true').sorted('roomUpdatedAt', true);
this.data.addListener(this.updateState);
} else if (this.props.searchText !== props.searchText) {
this.search(props.searchText);
@ -97,7 +96,7 @@ export default class RoomsListView extends React.Component {
});
}
let data = this.data.filtered('name CONTAINS[c] $0', searchText).slice(0, 7);
let data = database.objects('subscriptions').filtered('name CONTAINS[c] $0', searchText).slice(0, 7);
const usernames = data.map(sub => sub.map);
try {

View File

@ -1,5 +1,7 @@
import { StyleSheet, Dimensions, Platform } from 'react-native';
import { COLOR_DANGER } from '../constants/colors';
export default StyleSheet.create({
container: {
backgroundColor: 'white',
@ -56,7 +58,7 @@ export default StyleSheet.create({
color: '#2f343d'
},
label_error: {
color: 'red',
color: COLOR_DANGER,
flexGrow: 1,
paddingHorizontal: 0,
marginBottom: 20
@ -83,10 +85,14 @@ export default StyleSheet.create({
borderColor: 'rgba(0,0,0,.15)',
color: 'black'
},
buttonContainerLastChild: {
marginBottom: 40
},
buttonContainer: {
paddingVertical: 15,
backgroundColor: '#414852',
marginBottom: 20
marginBottom: 20,
borderRadius: 2
},
buttonContainer_white: {
paddingVertical: 15,
@ -117,7 +123,7 @@ export default StyleSheet.create({
},
error: {
textAlign: 'center',
color: 'red',
color: COLOR_DANGER,
paddingTop: 5
},
loading: {
@ -166,7 +172,7 @@ export default StyleSheet.create({
color: 'green'
},
invalidText: {
color: 'red'
color: COLOR_DANGER
},
validatingText: {
color: '#aaa'
@ -177,7 +183,7 @@ export default StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
margin: 4,
borderRadius: 4
borderRadius: 2
},
facebookButton: {
backgroundColor: '#3b5998'
@ -208,5 +214,24 @@ export default StyleSheet.create({
},
oAuthModal: {
margin: 0
},
status: {
position: 'absolute',
bottom: -3,
right: -3,
borderWidth: 3,
borderColor: '#fff',
borderRadius: 16,
width: 16,
height: 16
},
alignItemsFlexEnd: {
alignItems: 'flex-end'
},
textAlignRight: {
textAlign: 'right'
},
opacity5: {
opacity: 0.5
}
});

View File

@ -5,6 +5,7 @@
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
@ -49,6 +50,7 @@
74815BBCB91147C08C8F7B3D /* libRNAudio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1142E3442BA94B19BCF52814 /* libRNAudio.a */; };
77C35F50C01C43668188886C /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A0EEFAF8AB14F5B9E796CDD /* libRNVectorIcons.a */; };
7A430E4F20238C46008F55BC /* libRCTCustomInputController.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A430E1E20238C02008F55BC /* libRCTCustomInputController.a */; };
7AFB806E205AE65700D004E7 /* libRCTToast.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AFB804C205AE63100D004E7 /* libRCTToast.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
8A159EDB97C44E52AF62D69C /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DA50CE47374C4C35BE6D9D58 /* libRNSVG.a */; };
8ECBD927DDAC4987B98E102E /* libRCTVideo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 20CE3E407E0D4D9E8C9885F2 /* libRCTVideo.a */; };
@ -327,6 +329,13 @@
remoteGlobalIDString = 3D7682761D8E76B80014119E;
remoteInfo = SplashScreen;
};
7AFB804B205AE63100D004E7 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 7AFB8035205AE63000D004E7 /* RCTToast.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 327633421BFAAD7E004DA88E;
remoteInfo = RCTToast;
};
832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
@ -485,6 +494,7 @@
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
7A30DA4B2D474348824CD05B /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = "<group>"; };
7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTCustomInputController.xcodeproj; path = "../node_modules/react-native-keyboard-input/lib/ios/RCTCustomInputController.xcodeproj"; sourceTree = "<group>"; };
7AFB8035205AE63000D004E7 /* RCTToast.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTToast.xcodeproj; path = "../node_modules/@remobile/react-native-toast/ios/RCTToast.xcodeproj"; sourceTree = "<group>"; };
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
8A2DD67ADD954AD9873F45FC /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = "<group>"; };
9A1E1766CCB84C91A62BD5A6 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = "<group>"; };
@ -518,6 +528,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7AFB806E205AE65700D004E7 /* libRCTToast.a in Frameworks */,
B8971BB2202A093B0000D245 /* libKeyboardTrackingView.a in Frameworks */,
7A430E4F20238C46008F55BC /* libRCTCustomInputController.a in Frameworks */,
146834051AC3E58100842450 /* libReact.a in Frameworks */,
@ -756,9 +767,18 @@
name = Products;
sourceTree = "<group>";
};
7AFB8036205AE63000D004E7 /* Products */ = {
isa = PBXGroup;
children = (
7AFB804C205AE63100D004E7 /* libRCTToast.a */,
);
name = Products;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
7AFB8035205AE63000D004E7 /* RCTToast.xcodeproj */,
B8971BAC202A091D0000D245 /* KeyboardTrackingView.xcodeproj */,
7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */,
B88F58361FBF55E200B352B8 /* RCTPushNotification.xcodeproj */,
@ -1091,6 +1111,10 @@
ProductGroup = 832341B11AAA6A8300B99B32 /* Products */;
ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
},
{
ProductGroup = 7AFB8036205AE63000D004E7 /* Products */;
ProjectRef = 7AFB8035205AE63000D004E7 /* RCTToast.xcodeproj */;
},
{
ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */;
ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */;
@ -1400,6 +1424,13 @@
remoteRef = 7ADCFEBF1FEA8A7A00763ED8 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
7AFB804C205AE63100D004E7 /* libRCTToast.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libRCTToast.a;
remoteRef = 7AFB804B205AE63100D004E7 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
832341B51AAA6A8300B99B32 /* libRCTText.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@ -1771,6 +1802,7 @@
"$(SRCROOT)/../node_modules/react-native-splash-screen/ios",
"$(SRCROOT)/../node_modules/react-native-safari-view",
"$(SRCROOT)/../node_modules/react-native-audio/ios",
"$(SRCROOT)/../../../react-native/React/**",
);
INFOPLIST_FILE = RocketChatRN/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@ -1811,6 +1843,7 @@
"$(SRCROOT)/../node_modules/react-native-splash-screen/ios",
"$(SRCROOT)/../node_modules/react-native-safari-view",
"$(SRCROOT)/../node_modules/react-native-audio/ios",
"$(SRCROOT)/../../../react-native/React/**",
);
INFOPLIST_FILE = RocketChatRN/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";