Skip to content

Commit

Permalink
Merge f23a750 into 054b2df
Browse files Browse the repository at this point in the history
  • Loading branch information
kunall17 committed Jun 28, 2017
2 parents 054b2df + f23a750 commit adaf9cf
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 309 deletions.
1 change: 1 addition & 0 deletions src/api/registerForEvents.js
Expand Up @@ -28,6 +28,7 @@ export default (auth: Auth) =>
'muted_topics',
'update_display_settings',
'update_global_notifications',
'presence',
]),
},
);
254 changes: 254 additions & 0 deletions src/presence/__tests__/presenceReducers-test.js
@@ -0,0 +1,254 @@
/* @flow */
import {
PRESENCE_RESPONSE,
EVENT_PRESENCE,
ACCOUNT_SWITCH,
} from '../../actionConstants';
import presenceReducers, { activityFromPresence, timestampFromPresence } from '../presenceReducers';

const fiveSecsAgo = Math.floor(new Date() - 5) / 1000;

describe('presenceReducers', () => {
test('handles unknown action and no state by returning initial state', () => {
const newState = presenceReducers(undefined, {});
expect(newState).toBeDefined();
});

test('on unrecognized action, returns input state unchanged', () => {
const prevState = { hello: 'world' };
const newState = presenceReducers(prevState, {});
expect(newState).toEqual(prevState);
});

describe('activityFromPresence', () => {
test('when single presence, just returns status', () => {
const activity = activityFromPresence({
website: {
status: 'active',
},
});
expect(activity).toEqual('active');
});

test('when multiple presences, the most "active" beats "offline"', () => {
const activity = activityFromPresence({
website: {
status: 'offline',
},
mobile: {
status: 'active',
},
});
expect(activity).toEqual('active');
});

test('when multiple, the most "idle" beats "offline"', () => {
const activity = activityFromPresence({
website: {
status: 'idle',
},
mobile: {
status: 'offline',
},
});
expect(activity).toEqual('idle');
});
});

describe('timestampFromPresence', () => {
test('when single client just return timestamp', () => {
const activity = timestampFromPresence({
website: {
timestamp: 1475109413,
},
});
expect(activity).toEqual(1475109413);
});

test('when multiple clients return more recent timestamp', () => {
const activity = timestampFromPresence({
website: {
timestamp: 100,
},
mobile: {
timestamp: 200,
},
});
expect(activity).toEqual(200);
});
});

describe('PRESENCE_RESPONSE', () => {
test('merges a single user in presence response', () => {
const presence = {
'email@example.com': {
website: {
status: 'active',
timestamp: fiveSecsAgo,
},
},
};
const prevState = [
{
full_name: 'Some Guy',
email: 'email@example.com',
status: 'offline',
},
];
const expectedState = [
{
full_name: 'Some Guy',
email: 'email@example.com',
status: 'active',
timestamp: fiveSecsAgo,
},
];

const newState = presenceReducers(prevState, { type: PRESENCE_RESPONSE, presence });

expect(newState).toEqual(expectedState);
});

test('merges multiple users in presence response', () => {
const presence = {
'email@example.com': {
website: {
status: 'active',
timestamp: 1474527507,
client: 'website',
pushable: false,
},
},
'johndoe@example.com': {
website: {
status: 'active',
timestamp: fiveSecsAgo,
client: 'website',
pushable: false,
},
ZulipReactNative: {
status: 'active',
timestamp: 1475792205,
client: 'ZulipReactNative',
pushable: false,
},
ZulipAndroid: {
status: 'active',
timestamp: 1475455046,
client: 'ZulipAndroid',
pushable: false,
},
},
'janedoe@example.com': {
website: {
status: 'active',
timestamp: 1475792203,
client: 'website',
pushable: false,
},
ZulipAndroid: {
status: 'active',
timestamp: 1475109413,
client: 'ZulipAndroid',
pushable: false,
},
},
};
const prevState = [
{
full_name: 'Some Guy',
email: 'email@example.com',
status: 'offline',
},
{
full_name: 'John Doe',
email: 'johndoe@example.com',
status: 'offline',
},
{
full_name: 'Jane Doe',
email: 'janedoe@example.com',
status: 'offline',
},
];
const expectedState = [
{
full_name: 'Some Guy',
email: 'email@example.com',
status: 'offline',
timestamp: 1474527507,
},
{
full_name: 'John Doe',
email: 'johndoe@example.com',
status: 'active',
timestamp: fiveSecsAgo,
},
{
full_name: 'Jane Doe',
email: 'janedoe@example.com',
status: 'offline',
timestamp: 1475792203,
},
];

const newState = presenceReducers(prevState, { type: PRESENCE_RESPONSE, presence });

expect(newState).toEqual(expectedState);
});
});

describe('EVENT_PRESENCE', () => {
test('merges a single user presence', () => {
const prevState = [
{
full_name: 'Some Guy',
email: 'email@example.com',
status: 'offline',
},
];
const action = {
type: EVENT_PRESENCE,
email: 'email@example.com',
presence: {
website: {
status: 'active',
timestamp: fiveSecsAgo,
},
},
};
const expectedState = [
{
full_name: 'Some Guy',
email: 'email@example.com',
status: 'active',
timestamp: fiveSecsAgo,
},
];

const newState = presenceReducers(prevState, action);

expect(newState).toEqual(expectedState);
});
});

describe('ACCOUNT_SWITCH', () => {
test('resets state to initial state', () => {
const initialState = [
{
full_name: 'Some Guy',
email: 'email@example.com',
status: 'offline',
},
];
const action = {
type: ACCOUNT_SWITCH,
};
const expectedState = [];

const actualState = presenceReducers(initialState, action);

expect(actualState).toEqual(expectedState);
});
});
});
84 changes: 84 additions & 0 deletions src/presence/presenceReducers.js
@@ -0,0 +1,84 @@
/* @flow */
import { UsersState, Action, ClientPresence, Presence, UserStatus } from '../types';
import {
LOGOUT,
LOGIN_SUCCESS,
ACCOUNT_SWITCH,
EVENT_PRESENCE,
PRESENCE_RESPONSE,
} from '../actionConstants';

const priorityToState = [
'offline',
'idle',
'active',
];

const stateToPriority = {
offline: 0,
idle: 1,
active: 2,
};

export const activityFromPresence = (presence: ClientPresence): UserStatus =>
priorityToState[
Math.max(...Object.values(presence).map((x: Presence) => stateToPriority[x.status]))
];

export const timestampFromPresence = (presence: ClientPresence): UserStatus =>
Math.max(...Object.values(presence).map((x: Presence) => x.timestamp));

export const activityFromTimestamp = (activity: string, timestamp: number) =>
((new Date() / 1000) - timestamp > 60 ? 'offline' : activity);

const updateUserWithPresence = (user: Object, presence: Presence) => {
const timestamp = timestampFromPresence(presence);

return {
...user,
status: activityFromTimestamp(activityFromPresence(presence), timestamp),
timestamp,
};
};

const initialState: UsersState = [];

export default (state: UsersState = initialState, action: Action): UsersState => {
switch (action.type) {
case LOGOUT:
case LOGIN_SUCCESS:
case ACCOUNT_SWITCH:
return initialState;
case PRESENCE_RESPONSE: {
const newState = [...state];
return Object.keys(action.presence).reduce((currentState, email) => {
const userIndex = state.findIndex(u => u.email === email);
if (userIndex === -1) {
let user = { email };
user = updateUserWithPresence(user, action.presence[email]);
return [...currentState, user];
} else {
currentState[userIndex] = // eslint-disable-line
updateUserWithPresence(currentState[userIndex], action.presence[email]);
return currentState;
}
}, newState);
}
case EVENT_PRESENCE: {
const userIndex = state.findIndex(u => u.email === action.email);
let user;
let newState = [...state];
if (userIndex === -1) {
user = { email: action.email };
user = updateUserWithPresence(user, action.presence);
newState = [...state, user];
} else {
user = updateUserWithPresence(state[userIndex], action.presence);
newState[userIndex] = user;
}
return newState;
}
default:
return state;
}
};
2 changes: 2 additions & 0 deletions src/reducers.js
Expand Up @@ -12,6 +12,7 @@ import streams from './streamlist/streamsReducers';
import subscriptions from './subscriptions/subscriptionsReducers';
import typing from './typing/typingReducers';
import users from './users/usersReducers';
import presence from './presence/presenceReducers';

// Thanks to https://twitter.com/dan_abramov/status/656074974533459968?lang=en
const enableBatching = (reducer) =>
Expand All @@ -31,6 +32,7 @@ export default enableBatching(combineReducers({
flags,
mute,
nav,
presence,
realm,
settings,
streams,
Expand Down
2 changes: 1 addition & 1 deletion src/store.js
Expand Up @@ -15,7 +15,7 @@ const store = compose(

export const restore = (onFinished) =>
persistStore(store, {
blacklist: ['app'],
blacklist: ['app', 'presence'],
storage: AsyncStorage,
}, onFinished);

Expand Down

0 comments on commit adaf9cf

Please sign in to comment.