diff --git a/__mocks__/realm.js b/__mocks__/realm.js
new file mode 100644
index 00000000..8845406e
--- /dev/null
+++ b/__mocks__/realm.js
@@ -0,0 +1,26 @@
+export default class Realm {
+ schema = [];
+
+ data = [];
+
+ constructor(params) {
+ require('lodash').each(params.schema, (schema) => {
+ this.data[schema.name] = [];
+ this.data[schema.name].filtered = () => this.data[schema.name];
+ });
+ this.schema = params.schema;
+ }
+
+ objects(schemaName) {
+ return this.data[schemaName];
+ }
+
+ write = (fn) => {
+ fn();
+ }
+
+ create(schemaName, data) {
+ this.data[schemaName].push(data);
+ return data;
+ }
+}
diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js
index 776a58e9..3a0d3bbd 100644
--- a/app/actions/actionsTypes.js
+++ b/app/actions/actionsTypes.js
@@ -64,7 +64,6 @@ export const SERVER = createRequestTypes('SERVER', [
]);
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']);
export const LOGOUT = 'LOGOUT'; // logout is always success
-export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET']);
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
diff --git a/app/actions/activeUsers.js b/app/actions/activeUsers.js
deleted file mode 100644
index 1e7c5ecb..00000000
--- a/app/actions/activeUsers.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import * as types from './actionsTypes';
-
-export function setActiveUser(data) {
- return {
- type: types.ACTIVE_USERS.SET,
- data
- };
-}
diff --git a/app/containers/Status/index.js b/app/containers/Status/index.js
index 9951eebf..c1421ba7 100644
--- a/app/containers/Status/index.js
+++ b/app/containers/Status/index.js
@@ -4,28 +4,16 @@ import { connect } from 'react-redux';
import { ViewPropTypes } from 'react-native';
import Status from './Status';
+import database, { safeAddListener } from '../../lib/realm';
-@connect((state, ownProps) => {
- if (state.login.user && ownProps.id === state.login.user.id) {
- return {
- status: state.login.user && state.login.user.status,
- offline: !state.meteor.connected
- };
- }
-
- const user = state.activeUsers[ownProps.id];
- return {
- status: (user && user.status) || 'offline'
- };
-})
-
+@connect(state => ({
+ offline: !state.meteor.connected
+}))
export default class StatusContainer extends React.PureComponent {
static propTypes = {
- // id is a prop, but it's used only inside @connect to find for current status
- id: PropTypes.string, // eslint-disable-line
+ id: PropTypes.string,
style: ViewPropTypes.style,
size: PropTypes.number,
- status: PropTypes.string,
offline: PropTypes.bool
};
@@ -33,12 +21,32 @@ export default class StatusContainer extends React.PureComponent {
size: 16
}
+ constructor(props) {
+ super(props);
+ this.user = database.memoryDatabase.objects('activeUsers').filtered('id == $0', props.id);
+ this.state = {
+ user: this.user[0] || {}
+ };
+ safeAddListener(this.user, this.updateState);
+ }
+
+ componentWillUnmount() {
+ this.user.removeAllListeners();
+ }
+
get status() {
- const { offline, status } = this.props;
- if (offline) {
+ const { user } = this.state;
+ const { offline } = this.props;
+ if (offline || !user) {
return 'offline';
}
- return status;
+ return user.status || 'offline';
+ }
+
+ updateState = () => {
+ if (this.user.length) {
+ this.setState({ user: this.user[0] });
+ }
}
render() {
diff --git a/app/lib/realm.js b/app/lib/realm.js
index a3632734..db539637 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -331,6 +331,18 @@ const usersTypingSchema = {
}
};
+const activeUsersSchema = {
+ name: 'activeUsers',
+ primaryKey: 'id',
+ properties: {
+ id: 'string',
+ name: 'string?',
+ username: 'string?',
+ status: 'string?',
+ utcOffset: 'double?'
+ }
+};
+
const schema = [
settingsSchema,
subscriptionSchema,
@@ -353,7 +365,7 @@ const schema = [
uploadsSchema
];
-const inMemorySchema = [usersTypingSchema];
+const inMemorySchema = [usersTypingSchema, activeUsersSchema];
class DB {
databases = {
@@ -362,9 +374,9 @@ class DB {
schema: [
serversSchema
],
- schemaVersion: 5,
+ schemaVersion: 6,
migration: (oldRealm, newRealm) => {
- if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 5) {
+ if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 6) {
const newServers = newRealm.objects('servers');
// eslint-disable-next-line no-plusplus
@@ -377,7 +389,7 @@ class DB {
inMemoryDB: new Realm({
path: 'memory.realm',
schema: inMemorySchema,
- schemaVersion: 1,
+ schemaVersion: 2,
inMemory: true
})
}
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index f45fb599..a7990d1c 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -14,7 +14,6 @@ import {
setUser, setLoginServices, loginRequest, loginFailure, logout
} from '../actions/login';
import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
-import { setActiveUser } from '../actions/activeUsers';
import subscribeRooms from './methods/subscriptions/rooms';
import subscribeRoom from './methods/subscriptions/room';
@@ -119,18 +118,40 @@ const RocketChat = {
this._setUserTimer = setTimeout(() => {
const batchUsers = this.activeUsers;
InteractionManager.runAfterInteractions(() => {
- reduxStore.dispatch(setActiveUser(batchUsers));
+ database.memoryDatabase.write(() => {
+ Object.keys(batchUsers).forEach((key) => {
+ if (batchUsers[key] && batchUsers[key].id) {
+ try {
+ const data = batchUsers[key];
+ if (data.removed) {
+ const userRecord = database.memoryDatabase.objectForPrimaryKey('activeUsers', data.id);
+ if (userRecord) {
+ userRecord.status = 'offline';
+ }
+ } else {
+ database.memoryDatabase.create('activeUsers', data, true);
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ }
+ });
+ });
});
this._setUserTimer = null;
return this.activeUsers = {};
}, 10000);
}
- const activeUser = reduxStore.getState().activeUsers[ddpMessage.id];
if (!ddpMessage.fields) {
- this.activeUsers[ddpMessage.id] = {};
+ this.activeUsers[ddpMessage.id] = {
+ id: ddpMessage.id,
+ removed: true
+ };
} else {
- this.activeUsers[ddpMessage.id] = { ...this.activeUsers[ddpMessage.id], ...activeUser, ...ddpMessage.fields };
+ this.activeUsers[ddpMessage.id] = {
+ id: ddpMessage.id, ...this.activeUsers[ddpMessage.id], ...ddpMessage.fields
+ };
}
},
async loginSuccess({ user }) {
@@ -559,16 +580,15 @@ const RocketChat = {
// RC 0.48.0
return this.sdk.get('channels.info', { roomId });
},
- async getRoomMember(rid, currentUserId) {
- try {
- if (rid === `${ currentUserId }${ currentUserId }`) {
- return Promise.resolve(currentUserId);
- }
- const membersResult = await RocketChat.getRoomMembers(rid, true);
- return Promise.resolve(membersResult.records.find(m => m._id !== currentUserId));
- } catch (error) {
- return Promise.reject(error);
+ getUserInfo(userId) {
+ // RC 0.48.0
+ return this.sdk.get('users.info', { userId });
+ },
+ getRoomMemberId(rid, currentUserId) {
+ if (rid === `${ currentUserId }${ currentUserId }`) {
+ return currentUserId;
}
+ return rid.replace(currentUserId, '').trim();
},
toggleBlockUser(rid, blocked, block) {
if (block) {
diff --git a/app/reducers/activeUsers.js b/app/reducers/activeUsers.js
deleted file mode 100644
index 71aa7c4a..00000000
--- a/app/reducers/activeUsers.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as types from '../actions/actionsTypes';
-
-const initialState = {};
-
-export default (state = initialState, action) => {
- switch (action.type) {
- case types.ACTIVE_USERS.SET:
- return {
- ...state,
- ...action.data
- };
- default:
- return state;
- }
-};
diff --git a/app/reducers/index.js b/app/reducers/index.js
index 44b20942..ed4abc56 100644
--- a/app/reducers/index.js
+++ b/app/reducers/index.js
@@ -9,7 +9,6 @@ import selectedUsers from './selectedUsers';
import createChannel from './createChannel';
import app from './app';
import customEmojis from './customEmojis';
-import activeUsers from './activeUsers';
import sortPreferences from './sortPreferences';
export default combineReducers({
@@ -23,6 +22,5 @@ export default combineReducers({
app,
rooms,
customEmojis,
- activeUsers,
sortPreferences
});
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index a517cb71..31a4f385 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -331,8 +331,11 @@ export default class RoomActionsView extends LoggedView {
const { user } = this.props;
try {
- const member = await RocketChat.getRoomMember(rid, user.id);
- this.setState({ member: member || {} });
+ const roomUserId = RocketChat.getRoomMemberId(rid, user.id);
+ const result = await RocketChat.getUserInfo(roomUserId);
+ if (result.success) {
+ this.setState({ member: result.user });
+ }
} catch (e) {
log('RoomActions updateRoomMember', e);
this.setState({ member: {} });
@@ -400,7 +403,7 @@ export default class RoomActionsView extends LoggedView {
userId={user.id}
token={user.token}
>
- {t === 'd' ? : null }
+ {t === 'd' && member._id ? : null }
,
{room.t === 'd'
diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js
index d0b8e0f0..be89585b 100644
--- a/app/views/RoomInfoView/index.js
+++ b/app/views/RoomInfoView/index.js
@@ -4,7 +4,6 @@ import { View, Text, ScrollView } from 'react-native';
import { connect } from 'react-redux';
import moment from 'moment';
import { SafeAreaView } from 'react-navigation';
-import equal from 'deep-equal';
import LoggedView from '../View';
import Status from '../../containers/Status';
@@ -13,11 +12,11 @@ import styles from './styles';
import sharedStyles from '../Styles';
import database, { safeAddListener } from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
-import log from '../../utils/log';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n';
import { CustomHeaderButtons, Item } from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar';
+import log from '../../utils/log';
const PERMISSION_EDIT_ROOM = 'edit-room';
@@ -38,7 +37,6 @@ const getRoomTitle = room => (room.t === 'd'
id: state.login.user && state.login.user.id,
token: state.login.user && state.login.user.token
},
- activeUsers: state.activeUsers, // TODO: remove it
Message_TimeFormat: state.settings.Message_TimeFormat
}))
/** @extends React.Component */
@@ -65,22 +63,22 @@ export default class RoomInfoView extends LoggedView {
token: PropTypes.string
}),
baseUrl: PropTypes.string,
- activeUsers: PropTypes.object,
Message_TimeFormat: PropTypes.string
}
constructor(props) {
super('RoomInfoView', props);
- const rid = props.navigation.getParam('rid');
+ this.rid = props.navigation.getParam('rid');
const room = props.navigation.getParam('room');
- this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
+ this.t = props.navigation.getParam('t');
+ this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
+ this.roles = database.objects('roles');
this.sub = {
unsubscribe: () => {}
};
this.state = {
room: this.rooms[0] || room || {},
- roomUser: {},
- roles: []
+ roomUser: {}
};
}
@@ -93,70 +91,22 @@ export default class RoomInfoView extends LoggedView {
navigation.setParams({ showEdit: true });
}
- // get user of room
- if (room) {
- if (room.t === 'd') {
- try {
- const { user, activeUsers } = this.props;
- const roomUser = await RocketChat.getRoomMember(room.rid, user.id);
- this.setState({ roomUser: roomUser || {} });
- const username = room.name;
-
- const activeUser = 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(u => u.username === username);
- if (userRoles) {
- this.setState({ roles: userRoles.roles || [] });
- }
- } catch (e) {
- log('RoomInfoView.componentDidMount', e);
+ if (this.t === 'd') {
+ const { user } = this.props;
+ const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id);
+ try {
+ const result = await RocketChat.getUserInfo(roomUserId);
+ if (result.success) {
+ this.setState({ roomUser: result.user });
}
+ } catch (error) {
+ log('RoomInfoView.getUserInfo', error);
}
}
}
- shouldComponentUpdate(nextProps, nextState) {
- const {
- room, roomUser, roles
- } = this.state;
- const { activeUsers } = this.props;
- if (!equal(nextState.room, room)) {
- return true;
- }
- if (!equal(nextState.roomUser, roomUser)) {
- return true;
- }
- if (!equal(nextState.roles, roles)) {
- return true;
- }
- if (roomUser._id) {
- if (nextProps.activeUsers[roomUser._id] !== activeUsers[roomUser._id]) {
- return true;
- }
- }
- return false;
- }
-
componentWillUnmount() {
this.rooms.removeAllListeners();
- this.sub.unsubscribe();
- }
-
- getFullUserData = async(username) => {
- try {
- const result = await RocketChat.subscribe('fullUserData', username);
- this.sub = result;
- } catch (e) {
- log('getFullUserData', e);
- }
}
getRoleDescription = (id) => {
@@ -189,32 +139,47 @@ export default class RoomInfoView extends LoggedView {
);
- renderRoles = () => {
- const { roles } = this.state;
-
- return (
- roles.length > 0
- ? (
-
- {I18n.t('Roles')}
-
- {roles.map(role => (
-
- { this.getRoleDescription(role) }
-
- ))}
-
-
- )
- : null
- );
+ getRoleDescription = (id) => {
+ const role = database.objectForPrimaryKey('roles', id);
+ if (role) {
+ return role.description;
+ }
+ return null;
}
- renderTimezone = (userId) => {
- const { activeUsers, Message_TimeFormat } = this.props;
+ renderRole = (role) => {
+ const description = this.getRoleDescription(role);
+ if (description) {
+ return (
+
+ { this.getRoleDescription(role) }
+
+ );
+ }
+ return null;
+ }
- if (activeUsers[userId]) {
- const { utcOffset } = activeUsers[userId];
+ renderRoles = () => {
+ const { roomUser } = this.state;
+ if (roomUser && roomUser.roles && roomUser.roles.length) {
+ return (
+
+ {I18n.t('Roles')}
+
+ {roomUser.roles.map(role => this.renderRole(role))}
+
+
+ );
+ }
+ return null;
+ }
+
+ renderTimezone = () => {
+ const { roomUser } = this.state;
+ const { Message_TimeFormat } = this.props;
+
+ if (roomUser) {
+ const { utcOffset } = roomUser;
if (!utcOffset) {
return null;
@@ -242,7 +207,7 @@ export default class RoomInfoView extends LoggedView {
userId={user.id}
token={user.token}
>
- {room.t === 'd' ? : null}
+ {room.t === 'd' && roomUser._id ? : null}
);
}
@@ -258,12 +223,12 @@ export default class RoomInfoView extends LoggedView {
)
- renderCustomFields = (userId) => {
- const { activeUsers } = this.props;
- if (activeUsers[userId]) {
- const { customFields } = activeUsers[userId];
+ renderCustomFields = () => {
+ const { roomUser } = this.state;
+ if (roomUser) {
+ const { customFields } = roomUser;
- if (!customFields) {
+ if (!roomUser.customFields) {
return null;
}
@@ -301,7 +266,7 @@ export default class RoomInfoView extends LoggedView {
{!this.isDirect() ? this.renderItem('topic', room) : null}
{!this.isDirect() ? this.renderItem('announcement', room) : null}
{this.isDirect() ? this.renderRoles() : null}
- {this.isDirect() ? this.renderTimezone(roomUser._id) : null}
+ {this.isDirect() ? this.renderTimezone() : null}
{this.isDirect() ? this.renderCustomFields(roomUser._id) : null}
{room.broadcast ? this.renderBroadcast() : null}
diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js
index 6b680d84..54d0d4cd 100644
--- a/app/views/RoomView/Header/index.js
+++ b/app/views/RoomView/Header/index.js
@@ -4,28 +4,30 @@ import { connect } from 'react-redux';
import { responsive } from 'react-native-responsive-ui';
import equal from 'deep-equal';
-import database from '../../../lib/realm';
+import database, { safeAddListener } from '../../../lib/realm';
import Header from './Header';
import RightButtons from './RightButtons';
@responsive
@connect((state, ownProps) => {
- let status = '';
+ let status;
+ let userId;
+ let isLoggedUser = false;
const { rid, type } = ownProps;
if (type === 'd') {
if (state.login.user && state.login.user.id) {
const { id: loggedUserId } = state.login.user;
- const userId = rid.replace(loggedUserId, '').trim();
- if (userId === loggedUserId) {
+ userId = rid.replace(loggedUserId, '').trim();
+ isLoggedUser = userId === loggedUserId;
+ if (isLoggedUser) {
status = state.login.user.status; // eslint-disable-line
- } else {
- const user = state.activeUsers[userId];
- status = (user && user.status) || 'offline';
}
}
}
return {
+ userId,
+ isLoggedUser,
status
};
})
@@ -38,20 +40,28 @@ export default class RoomHeaderView extends Component {
rid: PropTypes.string,
window: PropTypes.object,
status: PropTypes.string,
- widthOffset: PropTypes.number
+ widthOffset: PropTypes.number,
+ isLoggedUser: PropTypes.bool,
+ userId: PropTypes.string
};
constructor(props) {
super(props);
this.usersTyping = database.memoryDatabase.objects('usersTyping').filtered('rid = $0', props.rid);
+ this.user = [];
+ if (props.type === 'd' && !props.isLoggedUser) {
+ this.user = database.memoryDatabase.objects('activeUsers').filtered('id == $0', props.userId);
+ safeAddListener(this.user, this.updateUser);
+ }
this.state = {
- usersTyping: this.usersTyping.slice() || []
+ usersTyping: this.usersTyping.slice() || [],
+ user: this.user[0] || {}
};
this.usersTyping.addListener(this.updateState);
}
shouldComponentUpdate(nextProps, nextState) {
- const { usersTyping } = this.state;
+ const { usersTyping, user } = this.state;
const {
type, title, status, window
} = this.props;
@@ -73,27 +83,36 @@ export default class RoomHeaderView extends Component {
if (!equal(nextState.usersTyping, usersTyping)) {
return true;
}
+ if (!equal(nextState.user, user)) {
+ return true;
+ }
return false;
}
- // componentDidUpdate(prevProps) {
- // if (isIOS) {
- // const { usersTyping } = this.props;
- // if (!equal(prevProps.usersTyping, usersTyping)) {
- // LayoutAnimation.easeInEaseOut();
- // }
- // }
- // }
-
updateState = () => {
this.setState({ usersTyping: this.usersTyping.slice() });
}
+ updateUser = () => {
+ if (this.user.length) {
+ this.setState({ user: this.user[0] });
+ }
+ }
+
render() {
- const { usersTyping } = this.state;
+ const { usersTyping, user } = this.state;
const {
- window, title, type, status, prid, tmid, widthOffset
+ window, title, type, prid, tmid, widthOffset, isLoggedUser, status: userStatus
} = this.props;
+ let status = 'offline';
+
+ if (type === 'd') {
+ if (isLoggedUser) {
+ status = userStatus;
+ } else {
+ status = user.status || 'offline';
+ }
+ }
return (
{
});
describe('Usage', async() => {
- it('should navigate to terms', async() => {
- await element(by.id('legal-terms-button')).tap();
- await waitFor(element(by.id('terms-view'))).toBeVisible().withTimeout(2000);
- await expect(element(by.id('terms-view'))).toBeVisible();
- });
+ // We can't simulate how webview behaves, so I had to disable :(
+ // it('should navigate to terms', async() => {
+ // await element(by.id('legal-terms-button')).tap();
+ // await waitFor(element(by.id('terms-view'))).toBeVisible().withTimeout(2000);
+ // await expect(element(by.id('terms-view'))).toBeVisible();
+ // });
- it('should navigate to privacy', async() => {
- await tapBack();
- await element(by.id('legal-privacy-button')).tap();
- await waitFor(element(by.id('privacy-view'))).toBeVisible().withTimeout(2000);
- await expect(element(by.id('privacy-view'))).toBeVisible();
- });
+ // it('should navigate to privacy', async() => {
+ // await tapBack();
+ // await element(by.id('legal-privacy-button')).tap();
+ // await waitFor(element(by.id('privacy-view'))).toBeVisible().withTimeout(2000);
+ // await expect(element(by.id('privacy-view'))).toBeVisible();
+ // });
it('should navigate to welcome', async() => {
await tapBack();
- await element(by.id('legal-view-close')).tap();
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('welcome-view'))).toBeVisible();
});