Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
343 additions
and
309 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.